1028 lines
47 KiB
TypeScript
1028 lines
47 KiB
TypeScript
|
|
import React, { useState, useEffect } from 'react';
|
||
|
|
import {
|
||
|
|
TrendingUp,
|
||
|
|
Briefcase,
|
||
|
|
DollarSign,
|
||
|
|
PieChart,
|
||
|
|
Edit3,
|
||
|
|
Trash2,
|
||
|
|
Plus,
|
||
|
|
RefreshCw,
|
||
|
|
Search,
|
||
|
|
CheckCircle,
|
||
|
|
AlertTriangle,
|
||
|
|
FileSpreadsheet,
|
||
|
|
Layers,
|
||
|
|
ArrowUpRight,
|
||
|
|
ArrowDownRight,
|
||
|
|
Sliders,
|
||
|
|
Building2,
|
||
|
|
X,
|
||
|
|
Check,
|
||
|
|
Zap,
|
||
|
|
Info,
|
||
|
|
ShieldCheck
|
||
|
|
} from 'lucide-react';
|
||
|
|
import { InvestmentSecurity, InvestmentCategory, InvestmentReconciliation, BranchId, User } from '../types';
|
||
|
|
import { BRANCHES_LIST } from '../types';
|
||
|
|
|
||
|
|
interface InvestmentsPortfolioViewProps {
|
||
|
|
currentUser: User | null;
|
||
|
|
activePeriod: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export const InvestmentsPortfolioView: React.FC<InvestmentsPortfolioViewProps> = ({
|
||
|
|
currentUser,
|
||
|
|
activePeriod,
|
||
|
|
}) => {
|
||
|
|
const isHO = currentUser?.role === 'admin';
|
||
|
|
const branchId = currentUser?.branchId;
|
||
|
|
|
||
|
|
const [investments, setInvestments] = useState<InvestmentSecurity[]>([]);
|
||
|
|
const [reconciliations, setReconciliations] = useState<InvestmentReconciliation[]>([]);
|
||
|
|
const [grandTotals, setGrandTotals] = useState<any>(null);
|
||
|
|
const [loading, setLoading] = useState<boolean>(true);
|
||
|
|
const [activeTab, setActiveTab] = useState<'securities' | 'reconciliation' | 'mtm_pricing'>('securities');
|
||
|
|
|
||
|
|
// Filters
|
||
|
|
const [searchQuery, setSearchQuery] = useState<string>('');
|
||
|
|
const [selectedBranchFilter, setSelectedBranchFilter] = useState<string>(isHO ? 'all' : branchId || 'all');
|
||
|
|
const [selectedCategoryFilter, setSelectedCategoryFilter] = useState<string>('all');
|
||
|
|
|
||
|
|
// Modals state
|
||
|
|
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
||
|
|
const [editingSecurity, setEditingSecurity] = useState<Partial<InvestmentSecurity> | null>(null);
|
||
|
|
|
||
|
|
// Bulk MTM Modal / Form state
|
||
|
|
const [bulkIsin, setBulkIsin] = useState<string>('');
|
||
|
|
const [bulkMtmPrice, setBulkMtmPrice] = useState<string>('');
|
||
|
|
const [bulkMtmYield, setBulkMtmYield] = useState<string>('');
|
||
|
|
const [mtmSuccessMsg, setMtmSuccessMsg] = useState<string>('');
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (!isHO && branchId) {
|
||
|
|
setSelectedBranchFilter(branchId);
|
||
|
|
}
|
||
|
|
}, [currentUser, isHO, branchId]);
|
||
|
|
|
||
|
|
const fetchInvestments = async () => {
|
||
|
|
setLoading(true);
|
||
|
|
try {
|
||
|
|
const url = !isHO && branchId ? `/api/investments?branchId=${branchId}` : (selectedBranchFilter !== 'all' ? `/api/investments?branchId=${selectedBranchFilter}` : '/api/investments');
|
||
|
|
const res = await fetch(url);
|
||
|
|
const data = await res.json();
|
||
|
|
setInvestments(data.investments || []);
|
||
|
|
|
||
|
|
// Fetch reconciliation
|
||
|
|
const reconRes = await fetch(`/api/investments/reconciliation?period=${activePeriod}`);
|
||
|
|
const reconData = await reconRes.json();
|
||
|
|
setReconciliations(reconData.reconciliations || []);
|
||
|
|
setGrandTotals(reconData.grandTotals || null);
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Failed to fetch investments:', err);
|
||
|
|
} finally {
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
fetchInvestments();
|
||
|
|
}, [currentUser, activePeriod, selectedBranchFilter]);
|
||
|
|
|
||
|
|
// Handle Add/Edit Security Save
|
||
|
|
const handleSaveSecurity = async (e: React.FormEvent) => {
|
||
|
|
e.preventDefault();
|
||
|
|
if (!editingSecurity) return;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const res = await fetch('/api/investments', {
|
||
|
|
method: 'POST',
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify({
|
||
|
|
...editingSecurity,
|
||
|
|
branchId: editingSecurity.branchId || branchId || 'bahrain',
|
||
|
|
faceValue: Number(editingSecurity.faceValue || 0),
|
||
|
|
amountInvested: Number(editingSecurity.amountInvested || 0),
|
||
|
|
couponRate: Number(editingSecurity.couponRate || 0),
|
||
|
|
originalPrice: Number(editingSecurity.originalPrice || 100),
|
||
|
|
purchaseYield: Number(editingSecurity.purchaseYield || 0),
|
||
|
|
}),
|
||
|
|
});
|
||
|
|
|
||
|
|
if (res.ok) {
|
||
|
|
setIsModalOpen(false);
|
||
|
|
setEditingSecurity(null);
|
||
|
|
fetchInvestments();
|
||
|
|
} else {
|
||
|
|
const err = await res.json();
|
||
|
|
alert(`Error: ${err.error || 'Failed to save security'}`);
|
||
|
|
}
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Save security error:', err);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Handle Delete Security
|
||
|
|
const handleDeleteSecurity = async (id: string) => {
|
||
|
|
if (!confirm('Are you sure you want to delete this security from the portfolio?')) return;
|
||
|
|
try {
|
||
|
|
const res = await fetch(`/api/investments/${id}`, { method: 'DELETE' });
|
||
|
|
if (res.ok) {
|
||
|
|
fetchInvestments();
|
||
|
|
}
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Delete security error:', err);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Handle HO Bulk MTM Price Update
|
||
|
|
const handleBulkMtmSubmit = async (e: React.FormEvent) => {
|
||
|
|
e.preventDefault();
|
||
|
|
if (!bulkIsin || !bulkMtmPrice) return;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const res = await fetch('/api/investments/bulk-mtm', {
|
||
|
|
method: 'POST',
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify({
|
||
|
|
isin: bulkIsin.trim(),
|
||
|
|
mtmPrice: Number(bulkMtmPrice),
|
||
|
|
mtmYield: bulkMtmYield ? Number(bulkMtmYield) : undefined,
|
||
|
|
updatedBy: currentUser?.name || 'Head Office Admin',
|
||
|
|
}),
|
||
|
|
});
|
||
|
|
|
||
|
|
if (res.ok) {
|
||
|
|
const data = await res.json();
|
||
|
|
setMtmSuccessMsg(data.message || 'MTM prices updated successfully');
|
||
|
|
setTimeout(() => setMtmSuccessMsg(''), 4000);
|
||
|
|
setBulkIsin('');
|
||
|
|
setBulkMtmPrice('');
|
||
|
|
setBulkMtmYield('');
|
||
|
|
fetchInvestments();
|
||
|
|
}
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Bulk MTM error:', err);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Filtered Securities
|
||
|
|
const filteredSecurities = investments.filter((sec) => {
|
||
|
|
const matchesBranch = selectedBranchFilter === 'all' || sec.branchId === selectedBranchFilter;
|
||
|
|
const matchesCategory = selectedCategoryFilter === 'all' || sec.category === selectedCategoryFilter;
|
||
|
|
const q = searchQuery.toLowerCase();
|
||
|
|
const matchesSearch =
|
||
|
|
!q ||
|
||
|
|
sec.isin?.toLowerCase().includes(q) ||
|
||
|
|
sec.securityType?.toLowerCase().includes(q) ||
|
||
|
|
sec.country?.toLowerCase().includes(q);
|
||
|
|
|
||
|
|
return matchesBranch && matchesCategory && matchesSearch;
|
||
|
|
});
|
||
|
|
|
||
|
|
// KPI Calculations
|
||
|
|
const totalFaceValue = filteredSecurities.reduce((acc, s) => acc + (s.faceValue || 0), 0);
|
||
|
|
const totalAmountInvested = filteredSecurities.reduce((acc, s) => acc + (s.amountInvested || 0), 0);
|
||
|
|
const totalMtmPnL = filteredSecurities.reduce((acc, s) => acc + (s.mtmPnL || 0), 0);
|
||
|
|
const totalMtmValue = filteredSecurities.reduce((acc, s) => {
|
||
|
|
const mtm = s.mtmPrice !== undefined ? (s.faceValue * s.mtmPrice) / 100 : s.amountInvested;
|
||
|
|
return acc + mtm;
|
||
|
|
}, 0);
|
||
|
|
|
||
|
|
// Unique ISINs count
|
||
|
|
const uniqueIsins = new Set(investments.map((s) => s.isin)).size;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="space-y-6">
|
||
|
|
{/* Top Banner / Header */}
|
||
|
|
<div className="holo-card p-6 rounded-2xl bg-slate-900/90 border border-slate-700/80 shadow-2xl relative overflow-hidden">
|
||
|
|
<div className="absolute top-0 right-0 w-96 h-96 bg-cyan-500/10 rounded-full blur-3xl pointer-events-none" />
|
||
|
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 relative z-10">
|
||
|
|
<div>
|
||
|
|
<div className="flex items-center gap-3">
|
||
|
|
<div className="p-3 bg-gradient-to-br from-cyan-500/20 to-blue-600/20 border border-cyan-500/30 rounded-xl text-cyan-400">
|
||
|
|
<Briefcase className="w-7 h-7" />
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<h1 className="text-2xl font-bold text-slate-100 flex items-center gap-2">
|
||
|
|
Investments Portfolio
|
||
|
|
<span className="text-xs px-2.5 py-0.5 rounded-full bg-cyan-500/10 border border-cyan-500/30 text-cyan-300 font-mono">
|
||
|
|
{isHO ? 'Global Consolidated' : `${currentUser?.branchId?.toUpperCase()} Branch`}
|
||
|
|
</span>
|
||
|
|
</h1>
|
||
|
|
<p className="text-sm text-slate-400 mt-0.5">
|
||
|
|
Complete portfolio management (HTM, AFS, HFT) with Head Office MTM revaluation & Balance Sheet reconciliation
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex items-center gap-3">
|
||
|
|
<button
|
||
|
|
onClick={fetchInvestments}
|
||
|
|
className="px-3.5 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700 text-sm font-medium flex items-center gap-2 transition-all"
|
||
|
|
>
|
||
|
|
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin text-cyan-400' : ''}`} />
|
||
|
|
Refresh
|
||
|
|
</button>
|
||
|
|
|
||
|
|
<button
|
||
|
|
onClick={() => {
|
||
|
|
setEditingSecurity({
|
||
|
|
branchId: (branchId || 'bahrain') as BranchId,
|
||
|
|
country: BRANCHES_LIST.find((b) => b.id === (branchId || 'bahrain'))?.country || 'Bahrain',
|
||
|
|
category: 'AFS',
|
||
|
|
isin: '',
|
||
|
|
securityType: '',
|
||
|
|
faceValue: 0,
|
||
|
|
amountInvested: 0,
|
||
|
|
couponRate: 0,
|
||
|
|
issueDate: '',
|
||
|
|
purchaseDate: '',
|
||
|
|
maturityDate: '',
|
||
|
|
originalPrice: 100,
|
||
|
|
});
|
||
|
|
setIsModalOpen(true);
|
||
|
|
}}
|
||
|
|
className="px-4 py-2 rounded-xl bg-gradient-to-r from-cyan-500 to-blue-600 hover:from-cyan-400 hover:to-blue-500 text-white font-medium text-sm shadow-lg shadow-cyan-500/20 flex items-center gap-2 transition-all"
|
||
|
|
>
|
||
|
|
<Plus className="w-4 h-4" />
|
||
|
|
Add Security
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* KPI Cards */}
|
||
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||
|
|
<div className="holo-card p-5 rounded-2xl bg-slate-900/80 border border-slate-800 hover:border-slate-700 transition-all">
|
||
|
|
<div className="flex items-center justify-between text-slate-400 text-xs font-semibold uppercase tracking-wider mb-2">
|
||
|
|
<span>Total Face Value</span>
|
||
|
|
<Layers className="w-4 h-4 text-cyan-400" />
|
||
|
|
</div>
|
||
|
|
<div className="text-2xl font-bold text-slate-100 font-mono">
|
||
|
|
USD {totalFaceValue.toFixed(2)} <span className="text-sm font-normal text-slate-400">Mn</span>
|
||
|
|
</div>
|
||
|
|
<div className="text-xs text-slate-400 mt-2 flex items-center gap-1">
|
||
|
|
<span className="text-cyan-400 font-semibold">{filteredSecurities.length}</span> active portfolio holdings
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="holo-card p-5 rounded-2xl bg-slate-900/80 border border-slate-800 hover:border-slate-700 transition-all">
|
||
|
|
<div className="flex items-center justify-between text-slate-400 text-xs font-semibold uppercase tracking-wider mb-2">
|
||
|
|
<span>Book Value (Invested)</span>
|
||
|
|
<DollarSign className="w-4 h-4 text-emerald-400" />
|
||
|
|
</div>
|
||
|
|
<div className="text-2xl font-bold text-emerald-400 font-mono">
|
||
|
|
USD {totalAmountInvested.toFixed(2)} <span className="text-sm font-normal text-slate-400">Mn</span>
|
||
|
|
</div>
|
||
|
|
<div className="text-xs text-slate-400 mt-2 flex items-center gap-1">
|
||
|
|
<span>Reconciles with Balance Sheet Investments</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="holo-card p-5 rounded-2xl bg-slate-900/80 border border-slate-800 hover:border-slate-700 transition-all">
|
||
|
|
<div className="flex items-center justify-between text-slate-400 text-xs font-semibold uppercase tracking-wider mb-2">
|
||
|
|
<span>MTM Market Value</span>
|
||
|
|
<TrendingUp className="w-4 h-4 text-purple-400" />
|
||
|
|
</div>
|
||
|
|
<div className="text-2xl font-bold text-purple-300 font-mono">
|
||
|
|
USD {totalMtmValue.toFixed(2)} <span className="text-sm font-normal text-slate-400">Mn</span>
|
||
|
|
</div>
|
||
|
|
<div className="text-xs text-slate-400 mt-2 flex items-center gap-1">
|
||
|
|
<span>HO Mark-to-Market revalued</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="holo-card p-5 rounded-2xl bg-slate-900/80 border border-slate-800 hover:border-slate-700 transition-all">
|
||
|
|
<div className="flex items-center justify-between text-slate-400 text-xs font-semibold uppercase tracking-wider mb-2">
|
||
|
|
<span>Unrealized MTM P&L</span>
|
||
|
|
{totalMtmPnL >= 0 ? (
|
||
|
|
<ArrowUpRight className="w-4 h-4 text-emerald-400" />
|
||
|
|
) : (
|
||
|
|
<ArrowDownRight className="w-4 h-4 text-rose-400" />
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
<div
|
||
|
|
className={`text-2xl font-bold font-mono ${
|
||
|
|
totalMtmPnL >= 0 ? 'text-emerald-400' : 'text-rose-400'
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
{totalMtmPnL >= 0 ? '+' : ''}${(totalMtmPnL / 1000000).toFixed(2)}{' '}
|
||
|
|
<span className="text-sm font-normal text-slate-400">Mn</span>
|
||
|
|
</div>
|
||
|
|
<div className="text-xs text-slate-400 mt-2 font-mono">
|
||
|
|
(${totalMtmPnL.toLocaleString('en-US', { maximumFractionDigits: 0 })})
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Navigation Tabs */}
|
||
|
|
<div className="flex items-center justify-between border-b border-slate-800 pb-3">
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<button
|
||
|
|
onClick={() => setActiveTab('securities')}
|
||
|
|
className={`px-4 py-2.5 rounded-xl font-medium text-sm flex items-center gap-2 transition-all ${
|
||
|
|
activeTab === 'securities'
|
||
|
|
? 'bg-cyan-500/20 text-cyan-300 border border-cyan-500/30'
|
||
|
|
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/50'
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
<Briefcase className="w-4 h-4" />
|
||
|
|
Securities Master ({filteredSecurities.length})
|
||
|
|
</button>
|
||
|
|
|
||
|
|
<button
|
||
|
|
onClick={() => setActiveTab('reconciliation')}
|
||
|
|
className={`px-4 py-2.5 rounded-xl font-medium text-sm flex items-center gap-2 transition-all ${
|
||
|
|
activeTab === 'reconciliation'
|
||
|
|
? 'bg-cyan-500/20 text-cyan-300 border border-cyan-500/30'
|
||
|
|
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/50'
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
<CheckCircle className="w-4 h-4" />
|
||
|
|
Balance Sheet Reconciliation
|
||
|
|
{grandTotals && !grandTotals.isMatched && (
|
||
|
|
<span className="w-2 h-2 rounded-full bg-amber-400 animate-ping" />
|
||
|
|
)}
|
||
|
|
</button>
|
||
|
|
|
||
|
|
{isHO && (
|
||
|
|
<button
|
||
|
|
onClick={() => setActiveTab('mtm_pricing')}
|
||
|
|
className={`px-4 py-2.5 rounded-xl font-medium text-sm flex items-center gap-2 transition-all ${
|
||
|
|
activeTab === 'mtm_pricing'
|
||
|
|
? 'bg-cyan-500/20 text-cyan-300 border border-cyan-500/30'
|
||
|
|
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/50'
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
<Zap className="w-4 h-4 text-amber-400" />
|
||
|
|
HO Bulk MTM Pricing Tool
|
||
|
|
</button>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="text-xs text-slate-400 hidden sm:block">
|
||
|
|
<span className="text-slate-300 font-semibold">{uniqueIsins}</span> Unique ISINs tracked
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* TAB 1: SECURITIES MASTER TABLE */}
|
||
|
|
{activeTab === 'securities' && (
|
||
|
|
<div className="space-y-4">
|
||
|
|
{/* Controls Bar */}
|
||
|
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 bg-slate-900/60 p-4 rounded-xl border border-slate-800">
|
||
|
|
<div className="flex-1 relative">
|
||
|
|
<Search className="w-4 h-4 text-slate-500 absolute left-3.5 top-1/2 -translate-y-1/2" />
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
placeholder="Search ISIN, Security Description, or Country..."
|
||
|
|
value={searchQuery}
|
||
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||
|
|
className="w-full pl-10 pr-4 py-2 rounded-xl bg-slate-950/80 border border-slate-700/80 text-slate-200 text-sm focus:outline-none focus:border-cyan-500"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex flex-wrap items-center gap-3">
|
||
|
|
{isHO ? (
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<span className="text-xs text-slate-400">Branch:</span>
|
||
|
|
<select
|
||
|
|
value={selectedBranchFilter}
|
||
|
|
onChange={(e) => setSelectedBranchFilter(e.target.value)}
|
||
|
|
className="py-2 px-3 rounded-xl bg-slate-950 border border-slate-700 text-slate-200 text-sm focus:outline-none focus:border-cyan-500"
|
||
|
|
>
|
||
|
|
<option value="all">All Branches</option>
|
||
|
|
{BRANCHES_LIST.map((b) => (
|
||
|
|
<option key={b.id} value={b.id}>
|
||
|
|
{b.name}
|
||
|
|
</option>
|
||
|
|
))}
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<div className="flex items-center gap-2 bg-slate-950 px-3 py-1.5 rounded-xl border border-cyan-500/30 text-xs text-cyan-300 font-mono">
|
||
|
|
<ShieldCheck className="w-3.5 h-3.5 text-cyan-400" />
|
||
|
|
<span>Branch Scope: {BRANCHES_LIST.find((b) => b.id === branchId)?.name || branchId?.toUpperCase()}</span>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<span className="text-xs text-slate-400">Category:</span>
|
||
|
|
<select
|
||
|
|
value={selectedCategoryFilter}
|
||
|
|
onChange={(e) => setSelectedCategoryFilter(e.target.value)}
|
||
|
|
className="py-2 px-3 rounded-xl bg-slate-950 border border-slate-700 text-slate-200 text-sm focus:outline-none focus:border-cyan-500"
|
||
|
|
>
|
||
|
|
<option value="all">All Categories</option>
|
||
|
|
<option value="HTM">HTM (Held-To-Maturity)</option>
|
||
|
|
<option value="AFS">AFS (Available-For-Sale)</option>
|
||
|
|
<option value="HFT">HFT (Held-For-Trading)</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Table */}
|
||
|
|
<div className="overflow-x-auto rounded-2xl border border-slate-800 bg-slate-900/80 shadow-xl">
|
||
|
|
<table className="w-full text-left border-collapse text-xs">
|
||
|
|
<thead>
|
||
|
|
<tr className="bg-slate-950/90 text-slate-400 font-semibold border-b border-slate-800 uppercase tracking-wider">
|
||
|
|
<th className="py-3.5 px-4">Branch / Country</th>
|
||
|
|
<th className="py-3.5 px-3">Cat</th>
|
||
|
|
<th className="py-3.5 px-3">ISIN / Security</th>
|
||
|
|
<th className="py-3.5 px-3 text-right">Face Value ($M)</th>
|
||
|
|
<th className="py-3.5 px-3 text-right">Amount Invested ($M)</th>
|
||
|
|
<th className="py-3.5 px-3 text-right">Coupon %</th>
|
||
|
|
<th className="py-3.5 px-3">Maturity Date</th>
|
||
|
|
<th className="py-3.5 px-3 text-right">Orig Price %</th>
|
||
|
|
<th className="py-3.5 px-3 text-right bg-cyan-950/30 text-cyan-300">HO MTM Price %</th>
|
||
|
|
<th className="py-3.5 px-3 text-right bg-cyan-950/30 text-cyan-300">MTM Yield %</th>
|
||
|
|
<th className="py-3.5 px-3 text-right">MTM P&L ($)</th>
|
||
|
|
<th className="py-3.5 px-3 text-center">Actions</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody className="divide-y divide-slate-800/60 font-mono text-slate-300">
|
||
|
|
{filteredSecurities.length === 0 ? (
|
||
|
|
<tr>
|
||
|
|
<td colSpan={12} className="py-12 text-center text-slate-500 font-sans">
|
||
|
|
No investment securities match the selected filters.
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
) : (
|
||
|
|
filteredSecurities.map((sec) => {
|
||
|
|
const branchObj = BRANCHES_LIST.find((b) => b.id === sec.branchId);
|
||
|
|
return (
|
||
|
|
<tr key={sec.id} className="hover:bg-slate-800/50 transition-colors">
|
||
|
|
<td className="py-3 px-4 font-sans font-medium text-slate-200">
|
||
|
|
<div>{branchObj?.name || sec.country}</div>
|
||
|
|
<div className="text-[10px] text-slate-500">{sec.country}</div>
|
||
|
|
</td>
|
||
|
|
|
||
|
|
<td className="py-3 px-3">
|
||
|
|
<span
|
||
|
|
className={`px-2 py-0.5 rounded text-[10px] font-semibold ${
|
||
|
|
sec.category === 'HTM'
|
||
|
|
? 'bg-amber-500/10 text-amber-300 border border-amber-500/30'
|
||
|
|
: sec.category === 'AFS'
|
||
|
|
? 'bg-cyan-500/10 text-cyan-300 border border-cyan-500/30'
|
||
|
|
: 'bg-purple-500/10 text-purple-300 border border-purple-500/30'
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
{sec.category}
|
||
|
|
</span>
|
||
|
|
</td>
|
||
|
|
|
||
|
|
<td className="py-3 px-3 font-sans">
|
||
|
|
<div className="font-mono text-cyan-400 font-semibold text-xs">{sec.isin}</div>
|
||
|
|
<div className="text-slate-300 text-[11px] truncate max-w-[200px]" title={sec.securityType}>
|
||
|
|
{sec.securityType}
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
|
||
|
|
<td className="py-3 px-3 text-right font-bold text-slate-100">
|
||
|
|
{sec.faceValue.toFixed(2)}
|
||
|
|
</td>
|
||
|
|
|
||
|
|
<td className="py-3 px-3 text-right font-bold text-emerald-400">
|
||
|
|
{sec.amountInvested.toFixed(2)}
|
||
|
|
</td>
|
||
|
|
|
||
|
|
<td className="py-3 px-3 text-right text-slate-300">
|
||
|
|
{sec.couponRate ? `${sec.couponRate.toFixed(2)}%` : '—'}
|
||
|
|
</td>
|
||
|
|
|
||
|
|
<td className="py-3 px-3 font-sans text-slate-400">{sec.maturityDate}</td>
|
||
|
|
|
||
|
|
<td className="py-3 px-3 text-right text-slate-300">
|
||
|
|
{sec.originalPrice ? sec.originalPrice.toFixed(3) : '100.000'}
|
||
|
|
</td>
|
||
|
|
|
||
|
|
<td className="py-3 px-3 text-right font-bold text-cyan-300 bg-cyan-950/20">
|
||
|
|
{sec.mtmPrice !== undefined ? sec.mtmPrice.toFixed(3) : '—'}
|
||
|
|
</td>
|
||
|
|
|
||
|
|
<td className="py-3 px-3 text-right text-cyan-200 bg-cyan-950/20">
|
||
|
|
{sec.mtmYield !== undefined ? `${sec.mtmYield.toFixed(2)}%` : '—'}
|
||
|
|
</td>
|
||
|
|
|
||
|
|
<td
|
||
|
|
className={`py-3 px-3 text-right font-bold ${
|
||
|
|
(sec.mtmPnL || 0) >= 0 ? 'text-emerald-400' : 'text-rose-400'
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
{sec.mtmPnL !== undefined
|
||
|
|
? `${sec.mtmPnL >= 0 ? '+' : ''}$${Math.round(sec.mtmPnL).toLocaleString()}`
|
||
|
|
: '—'}
|
||
|
|
</td>
|
||
|
|
|
||
|
|
<td className="py-3 px-3 text-center font-sans">
|
||
|
|
<div className="flex items-center justify-center gap-1.5">
|
||
|
|
<button
|
||
|
|
onClick={() => {
|
||
|
|
setEditingSecurity(sec);
|
||
|
|
setIsModalOpen(true);
|
||
|
|
}}
|
||
|
|
className="p-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-cyan-400 transition-colors"
|
||
|
|
title="Edit Security"
|
||
|
|
>
|
||
|
|
<Edit3 className="w-3.5 h-3.5" />
|
||
|
|
</button>
|
||
|
|
|
||
|
|
<button
|
||
|
|
onClick={() => handleDeleteSecurity(sec.id)}
|
||
|
|
className="p-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-rose-400 transition-colors"
|
||
|
|
title="Delete Security"
|
||
|
|
>
|
||
|
|
<Trash2 className="w-3.5 h-3.5" />
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
);
|
||
|
|
})
|
||
|
|
)}
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* TAB 2: RECONCILIATION MATRIX */}
|
||
|
|
{activeTab === 'reconciliation' && (
|
||
|
|
<div className="space-y-4">
|
||
|
|
<div className="bg-slate-900/80 p-5 rounded-2xl border border-slate-800">
|
||
|
|
<div className="flex items-center justify-between mb-4">
|
||
|
|
<div>
|
||
|
|
<h3 className="text-lg font-bold text-slate-100 flex items-center gap-2">
|
||
|
|
<CheckCircle className="w-5 h-5 text-cyan-400" />
|
||
|
|
Portfolio vs Balance Sheet Reconciliation
|
||
|
|
</h3>
|
||
|
|
<p className="text-xs text-slate-400 mt-0.5">
|
||
|
|
Verifies that each branch's detailed Investment Portfolio sum matches its Balance Sheet 'Investments' line item for period {activePeriod}.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{grandTotals && (
|
||
|
|
<div className="flex items-center gap-3 font-mono text-xs">
|
||
|
|
<span className="px-3 py-1.5 rounded-xl bg-slate-950 border border-slate-800 text-slate-300">
|
||
|
|
Grand Portfolio Total: <strong className="text-cyan-400">${grandTotals.portfolioTotalInvested}M</strong>
|
||
|
|
</span>
|
||
|
|
<span className="px-3 py-1.5 rounded-xl bg-slate-950 border border-slate-800 text-slate-300">
|
||
|
|
Grand BS Total: <strong className="text-emerald-400">${grandTotals.balanceSheetInvestments}M</strong>
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="overflow-x-auto rounded-xl border border-slate-800">
|
||
|
|
<table className="w-full text-left border-collapse text-xs font-sans">
|
||
|
|
<thead>
|
||
|
|
<tr className="bg-slate-950 text-slate-400 font-semibold border-b border-slate-800 uppercase tracking-wider">
|
||
|
|
<th className="py-3 px-4">Branch Name</th>
|
||
|
|
<th className="py-3 px-3 text-center">Securities Count</th>
|
||
|
|
<th className="py-3 px-3 text-right">Portfolio Invested ($M)</th>
|
||
|
|
<th className="py-3 px-3 text-right">Balance Sheet 'Investments' ($M)</th>
|
||
|
|
<th className="py-3 px-3 text-right">Variance ($M)</th>
|
||
|
|
<th className="py-3 px-4 text-center">Reconciliation Status</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody className="divide-y divide-slate-800/60 font-mono">
|
||
|
|
{reconciliations.map((recon) => (
|
||
|
|
<tr key={recon.branchId} className="hover:bg-slate-800/40 transition-colors">
|
||
|
|
<td className="py-3 px-4 font-sans font-semibold text-slate-200">
|
||
|
|
{recon.branchName}
|
||
|
|
</td>
|
||
|
|
|
||
|
|
<td className="py-3 px-3 text-center text-slate-400">
|
||
|
|
{recon.securitiesCount}
|
||
|
|
</td>
|
||
|
|
|
||
|
|
<td className="py-3 px-3 text-right font-bold text-cyan-300">
|
||
|
|
${recon.portfolioTotalInvested.toFixed(2)}
|
||
|
|
</td>
|
||
|
|
|
||
|
|
<td className="py-3 px-3 text-right font-bold text-emerald-400">
|
||
|
|
${recon.balanceSheetInvestments.toFixed(2)}
|
||
|
|
</td>
|
||
|
|
|
||
|
|
<td
|
||
|
|
className={`py-3 px-3 text-right font-bold ${
|
||
|
|
recon.difference === 0
|
||
|
|
? 'text-slate-400'
|
||
|
|
: recon.difference > 0
|
||
|
|
? 'text-cyan-400'
|
||
|
|
: 'text-amber-400'
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
{recon.difference === 0
|
||
|
|
? '$0.00'
|
||
|
|
: `${recon.difference > 0 ? '+' : ''}$${recon.difference.toFixed(2)}`}
|
||
|
|
</td>
|
||
|
|
|
||
|
|
<td className="py-3 px-4 text-center font-sans">
|
||
|
|
{recon.isMatched ? (
|
||
|
|
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-semibold bg-emerald-500/10 border border-emerald-500/30 text-emerald-400">
|
||
|
|
<Check className="w-3.5 h-3.5" />
|
||
|
|
Reconciled
|
||
|
|
</span>
|
||
|
|
) : (
|
||
|
|
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-semibold bg-amber-500/10 border border-amber-500/30 text-amber-300">
|
||
|
|
<AlertTriangle className="w-3.5 h-3.5" />
|
||
|
|
Mismatch (${Math.abs(recon.difference).toFixed(2)}M)
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
))}
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* TAB 3: HO BULK MTM PRICING TOOL */}
|
||
|
|
{activeTab === 'mtm_pricing' && isHO && (
|
||
|
|
<div className="space-y-6">
|
||
|
|
<div className="holo-card p-6 rounded-2xl bg-slate-900/90 border border-slate-800">
|
||
|
|
<h3 className="text-lg font-bold text-slate-100 flex items-center gap-2 mb-2">
|
||
|
|
<Zap className="w-5 h-5 text-amber-400" />
|
||
|
|
Head Office Mark-to-Market (MTM) Revaluation Feed
|
||
|
|
</h3>
|
||
|
|
<p className="text-xs text-slate-400 mb-6">
|
||
|
|
Input MTM prices (% of par) or Yields for specific ISIN securities. All branches holding securities with matching ISIN will be updated and revalued automatically.
|
||
|
|
</p>
|
||
|
|
|
||
|
|
{mtmSuccessMsg && (
|
||
|
|
<div className="p-3.5 mb-6 rounded-xl bg-emerald-500/10 border border-emerald-500/30 text-emerald-300 text-sm flex items-center gap-2 font-medium">
|
||
|
|
<Check className="w-4 h-4 text-emerald-400" />
|
||
|
|
{mtmSuccessMsg}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<form onSubmit={handleBulkMtmSubmit} className="grid grid-cols-1 md:grid-cols-4 gap-4 items-end">
|
||
|
|
<div>
|
||
|
|
<label className="block text-xs font-semibold text-slate-300 mb-1.5">
|
||
|
|
Select / Enter ISIN Code
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
placeholder="e.g. XS2232319638"
|
||
|
|
value={bulkIsin}
|
||
|
|
onChange={(e) => setBulkIsin(e.target.value)}
|
||
|
|
required
|
||
|
|
className="w-full px-3.5 py-2.5 rounded-xl bg-slate-950 border border-slate-700 font-mono text-cyan-300 text-sm focus:outline-none focus:border-cyan-500"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<label className="block text-xs font-semibold text-slate-300 mb-1.5">
|
||
|
|
New MTM Price (% of Par)
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
type="number"
|
||
|
|
step="0.001"
|
||
|
|
placeholder="e.g. 100.380"
|
||
|
|
value={bulkMtmPrice}
|
||
|
|
onChange={(e) => setBulkMtmPrice(e.target.value)}
|
||
|
|
required
|
||
|
|
className="w-full px-3.5 py-2.5 rounded-xl bg-slate-950 border border-slate-700 font-mono text-emerald-400 text-sm focus:outline-none focus:border-cyan-500"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<label className="block text-xs font-semibold text-slate-300 mb-1.5">
|
||
|
|
MTM Yield % (Optional)
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
type="number"
|
||
|
|
step="0.01"
|
||
|
|
placeholder="e.g. 7.28"
|
||
|
|
value={bulkMtmYield}
|
||
|
|
onChange={(e) => setBulkMtmYield(e.target.value)}
|
||
|
|
className="w-full px-3.5 py-2.5 rounded-xl bg-slate-950 border border-slate-700 font-mono text-purple-300 text-sm focus:outline-none focus:border-cyan-500"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<button
|
||
|
|
type="submit"
|
||
|
|
className="w-full py-2.5 px-4 rounded-xl bg-gradient-to-r from-amber-500 to-orange-600 hover:from-amber-400 hover:to-orange-500 text-white font-semibold text-sm shadow-lg shadow-amber-500/20 flex items-center justify-center gap-2 transition-all"
|
||
|
|
>
|
||
|
|
<Zap className="w-4 h-4" />
|
||
|
|
Apply MTM Revaluation
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</form>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Preset MTM Market Feed Quotes */}
|
||
|
|
<div className="bg-slate-900/80 p-5 rounded-2xl border border-slate-800">
|
||
|
|
<h4 className="text-sm font-bold text-slate-200 mb-3 flex items-center justify-between">
|
||
|
|
<span>Bloomberg / Market Benchmark Reference Quotes (30-Jun-26 Feed)</span>
|
||
|
|
<span className="text-xs font-normal text-slate-400">Click quote to auto-fill</span>
|
||
|
|
</h4>
|
||
|
|
|
||
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||
|
|
{[
|
||
|
|
{ isin: 'XS2232319638', name: 'GOP EuroBond 2031', price: 100.38, yield: 7.28 },
|
||
|
|
{ isin: 'XS1729875598', name: 'GOP EuroBond 2027', price: 100.50, yield: 6.50 },
|
||
|
|
{ isin: 'XS3353874145', name: 'GOP EuroBond 2029', price: 99.75, yield: 7.07 },
|
||
|
|
{ isin: 'XS2419405274', name: 'PAKISTAN SUKUK 7.95%', price: 99.99, yield: 7.95 },
|
||
|
|
{ isin: 'BD927251100', name: 'Bangladesh 10Y T-Bond', price: 98.45, yield: 9.71 },
|
||
|
|
{ isin: 'BD934481203', name: 'Bangladesh 20Y T-Bond', price: 109.90, yield: 10.27 },
|
||
|
|
].map((feed) => (
|
||
|
|
<button
|
||
|
|
key={feed.isin}
|
||
|
|
onClick={() => {
|
||
|
|
setBulkIsin(feed.isin);
|
||
|
|
setBulkMtmPrice(feed.price.toString());
|
||
|
|
setBulkMtmYield(feed.yield.toString());
|
||
|
|
}}
|
||
|
|
className="p-3 rounded-xl bg-slate-950 border border-slate-800 hover:border-cyan-500/50 text-left transition-all group"
|
||
|
|
>
|
||
|
|
<div className="flex items-center justify-between mb-1">
|
||
|
|
<span className="font-mono text-xs font-bold text-cyan-400 group-hover:text-cyan-300">
|
||
|
|
{feed.isin}
|
||
|
|
</span>
|
||
|
|
<span className="text-[10px] text-slate-500">Live Quote</span>
|
||
|
|
</div>
|
||
|
|
<div className="text-xs text-slate-200 font-medium">{feed.name}</div>
|
||
|
|
<div className="mt-2 flex items-center justify-between text-xs font-mono">
|
||
|
|
<span className="text-emerald-400">Price: {feed.price}%</span>
|
||
|
|
<span className="text-purple-300">Yield: {feed.yield}%</span>
|
||
|
|
</div>
|
||
|
|
</button>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* MODAL: ADD / EDIT SECURITY */}
|
||
|
|
{isModalOpen && editingSecurity && (
|
||
|
|
<div className="fixed inset-0 z-50 bg-slate-950/80 backdrop-blur-md flex items-center justify-center p-4">
|
||
|
|
<div className="bg-slate-900 border border-slate-700 rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto p-6 shadow-2xl relative">
|
||
|
|
<button
|
||
|
|
onClick={() => setIsModalOpen(false)}
|
||
|
|
className="absolute top-4 right-4 p-2 rounded-xl bg-slate-800 text-slate-400 hover:text-slate-100"
|
||
|
|
>
|
||
|
|
<X className="w-5 h-5" />
|
||
|
|
</button>
|
||
|
|
|
||
|
|
<h3 className="text-xl font-bold text-slate-100 mb-1">
|
||
|
|
{editingSecurity.id ? 'Edit Investment Security' : 'Add Investment Security'}
|
||
|
|
</h3>
|
||
|
|
<p className="text-xs text-slate-400 mb-5">
|
||
|
|
Enter complete security details for portfolio tracking & reconciliation.
|
||
|
|
</p>
|
||
|
|
|
||
|
|
<form onSubmit={handleSaveSecurity} className="space-y-4">
|
||
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||
|
|
<div>
|
||
|
|
<label className="block text-xs font-semibold text-slate-300 mb-1">
|
||
|
|
Branch / Territory
|
||
|
|
</label>
|
||
|
|
<select
|
||
|
|
value={editingSecurity.branchId || branchId || 'bahrain'}
|
||
|
|
onChange={(e) =>
|
||
|
|
setEditingSecurity({
|
||
|
|
...editingSecurity,
|
||
|
|
branchId: e.target.value as BranchId,
|
||
|
|
country: BRANCHES_LIST.find((b) => b.id === e.target.value)?.country || 'Bahrain',
|
||
|
|
})
|
||
|
|
}
|
||
|
|
disabled={!isHO}
|
||
|
|
className="w-full px-3 py-2 rounded-xl bg-slate-950 border border-slate-700 text-slate-200 text-sm"
|
||
|
|
>
|
||
|
|
{BRANCHES_LIST.map((b) => (
|
||
|
|
<option key={b.id} value={b.id}>
|
||
|
|
{b.name} ({b.country})
|
||
|
|
</option>
|
||
|
|
))}
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<label className="block text-xs font-semibold text-slate-300 mb-1">
|
||
|
|
Category
|
||
|
|
</label>
|
||
|
|
<select
|
||
|
|
value={editingSecurity.category || 'AFS'}
|
||
|
|
onChange={(e) =>
|
||
|
|
setEditingSecurity({
|
||
|
|
...editingSecurity,
|
||
|
|
category: e.target.value as InvestmentCategory,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
className="w-full px-3 py-2 rounded-xl bg-slate-950 border border-slate-700 text-slate-200 text-sm"
|
||
|
|
>
|
||
|
|
<option value="HTM">HTM (Held-To-Maturity)</option>
|
||
|
|
<option value="AFS">AFS (Available-For-Sale)</option>
|
||
|
|
<option value="HFT">HFT (Held-For-Trading)</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<label className="block text-xs font-semibold text-slate-300 mb-1">
|
||
|
|
ISIN Code
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
required
|
||
|
|
placeholder="e.g. XS2232319638"
|
||
|
|
value={editingSecurity.isin || ''}
|
||
|
|
onChange={(e) => setEditingSecurity({ ...editingSecurity, isin: e.target.value })}
|
||
|
|
className="w-full px-3 py-2 rounded-xl bg-slate-950 border border-slate-700 font-mono text-cyan-300 text-sm"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<label className="block text-xs font-semibold text-slate-300 mb-1">
|
||
|
|
Security Description
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
required
|
||
|
|
placeholder="e.g. GOP EuroBond 2029"
|
||
|
|
value={editingSecurity.securityType || ''}
|
||
|
|
onChange={(e) => setEditingSecurity({ ...editingSecurity, securityType: e.target.value })}
|
||
|
|
className="w-full px-3 py-2 rounded-xl bg-slate-950 border border-slate-700 text-slate-200 text-sm"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<label className="block text-xs font-semibold text-slate-300 mb-1">
|
||
|
|
Deal Currency
|
||
|
|
</label>
|
||
|
|
<select
|
||
|
|
value={editingSecurity.currency || 'USD'}
|
||
|
|
onChange={(e) =>
|
||
|
|
setEditingSecurity({
|
||
|
|
...editingSecurity,
|
||
|
|
currency: e.target.value,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
className="w-full px-3 py-2 rounded-xl bg-slate-950 border border-slate-700 text-slate-200 text-sm font-mono"
|
||
|
|
>
|
||
|
|
<option value="USD">USD - US Dollar</option>
|
||
|
|
<option value="PKR">PKR - Pakistani Rupee</option>
|
||
|
|
<option value="EUR">EUR - Euro</option>
|
||
|
|
<option value="GBP">GBP - British Pound</option>
|
||
|
|
<option value="JPY">JPY - Japanese Yen</option>
|
||
|
|
<option value="CAD">CAD - Canadian Dollar</option>
|
||
|
|
<option value="SAR">SAR - Saudi Riyal</option>
|
||
|
|
<option value="AED">AED - UAE Dirham</option>
|
||
|
|
<option value="BDT">BDT - Bangladeshi Taka</option>
|
||
|
|
<option value="CNY">CNY - Chinese Yuan</option>
|
||
|
|
<option value="HKD">HKD - Hong Kong Dollar</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<label className="block text-xs font-semibold text-slate-300 mb-1">
|
||
|
|
Face Value (Mio in Deal Currency)
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
type="number"
|
||
|
|
step="0.01"
|
||
|
|
required
|
||
|
|
placeholder="e.g. 10.00"
|
||
|
|
value={editingSecurity.faceValue ?? ''}
|
||
|
|
onChange={(e) => setEditingSecurity({ ...editingSecurity, faceValue: Number(e.target.value) })}
|
||
|
|
className="w-full px-3 py-2 rounded-xl bg-slate-950 border border-slate-700 font-mono text-slate-100 text-sm"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<label className="block text-xs font-semibold text-slate-300 mb-1">
|
||
|
|
Amount Invested / Book Value (USD Mio)
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
type="number"
|
||
|
|
step="0.01"
|
||
|
|
required
|
||
|
|
placeholder="e.g. 10.17"
|
||
|
|
value={editingSecurity.amountInvested ?? ''}
|
||
|
|
onChange={(e) =>
|
||
|
|
setEditingSecurity({ ...editingSecurity, amountInvested: Number(e.target.value) })
|
||
|
|
}
|
||
|
|
className="w-full px-3 py-2 rounded-xl bg-slate-950 border border-slate-700 font-mono text-emerald-400 text-sm"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<label className="block text-xs font-semibold text-slate-300 mb-1">
|
||
|
|
Coupon Rate (%)
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
type="number"
|
||
|
|
step="0.01"
|
||
|
|
placeholder="e.g. 7.38"
|
||
|
|
value={editingSecurity.couponRate ?? ''}
|
||
|
|
onChange={(e) => setEditingSecurity({ ...editingSecurity, couponRate: Number(e.target.value) })}
|
||
|
|
className="w-full px-3 py-2 rounded-xl bg-slate-950 border border-slate-700 font-mono text-slate-200 text-sm"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<label className="block text-xs font-semibold text-slate-300 mb-1">
|
||
|
|
Original Price (% of Par)
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
type="number"
|
||
|
|
step="0.001"
|
||
|
|
placeholder="e.g. 101.700"
|
||
|
|
value={editingSecurity.originalPrice ?? ''}
|
||
|
|
onChange={(e) =>
|
||
|
|
setEditingSecurity({ ...editingSecurity, originalPrice: Number(e.target.value) })
|
||
|
|
}
|
||
|
|
className="w-full px-3 py-2 rounded-xl bg-slate-950 border border-slate-700 font-mono text-slate-200 text-sm"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<label className="block text-xs font-semibold text-slate-300 mb-1">
|
||
|
|
Purchase / Issue Date
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
placeholder="e.g. 13-Jul-21"
|
||
|
|
value={editingSecurity.purchaseDate || ''}
|
||
|
|
onChange={(e) => setEditingSecurity({ ...editingSecurity, purchaseDate: e.target.value })}
|
||
|
|
className="w-full px-3 py-2 rounded-xl bg-slate-950 border border-slate-700 text-slate-200 text-sm"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<label className="block text-xs font-semibold text-slate-300 mb-1">
|
||
|
|
Maturity Date
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
placeholder="e.g. 08-Apr-31"
|
||
|
|
value={editingSecurity.maturityDate || ''}
|
||
|
|
onChange={(e) => setEditingSecurity({ ...editingSecurity, maturityDate: e.target.value })}
|
||
|
|
className="w-full px-3 py-2 rounded-xl bg-slate-950 border border-slate-700 text-slate-200 text-sm"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{isHO && (
|
||
|
|
<div className="p-4 rounded-xl bg-slate-950 border border-slate-800 space-y-3 mt-2">
|
||
|
|
<div className="text-xs font-semibold text-cyan-400">Head Office Valuation (MTM)</div>
|
||
|
|
<div className="grid grid-cols-2 gap-3">
|
||
|
|
<div>
|
||
|
|
<label className="block text-[11px] text-slate-400 mb-1">MTM Price (%)</label>
|
||
|
|
<input
|
||
|
|
type="number"
|
||
|
|
step="0.001"
|
||
|
|
placeholder="e.g. 100.380"
|
||
|
|
value={editingSecurity.mtmPrice ?? ''}
|
||
|
|
onChange={(e) =>
|
||
|
|
setEditingSecurity({ ...editingSecurity, mtmPrice: Number(e.target.value) })
|
||
|
|
}
|
||
|
|
className="w-full px-3 py-1.5 rounded-lg bg-slate-900 border border-slate-700 font-mono text-cyan-300 text-xs"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<label className="block text-[11px] text-slate-400 mb-1">MTM Yield (%)</label>
|
||
|
|
<input
|
||
|
|
type="number"
|
||
|
|
step="0.01"
|
||
|
|
placeholder="e.g. 7.28"
|
||
|
|
value={editingSecurity.mtmYield ?? ''}
|
||
|
|
onChange={(e) =>
|
||
|
|
setEditingSecurity({ ...editingSecurity, mtmYield: Number(e.target.value) })
|
||
|
|
}
|
||
|
|
className="w-full px-3 py-1.5 rounded-lg bg-slate-900 border border-slate-700 font-mono text-purple-300 text-xs"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<div className="flex items-center justify-end gap-3 pt-4 border-t border-slate-800">
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={() => setIsModalOpen(false)}
|
||
|
|
className="px-4 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 text-sm"
|
||
|
|
>
|
||
|
|
Cancel
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
type="submit"
|
||
|
|
className="px-5 py-2 rounded-xl bg-gradient-to-r from-cyan-500 to-blue-600 hover:from-cyan-400 hover:to-blue-500 text-white font-semibold text-sm shadow-lg shadow-cyan-500/20"
|
||
|
|
>
|
||
|
|
Save Security
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</form>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|