Files
NeoCom/src/app/(dashboard)/finance/consolidation/intercompany/page.tsx
2026-04-09 20:36:10 -07:00

399 lines
17 KiB
TypeScript

'use client';
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { formatCurrency, formatCompactCurrency } from '@/lib/utils';
import {
ArrowLeft,
GitMerge,
Loader2,
RefreshCcw,
ArrowRight,
Building2,
FileText,
Search,
AlertCircle,
} from 'lucide-react';
interface Entity {
id: string;
name: string;
code: string;
type: string;
defaultCurrency: string;
status: string;
}
interface ICTransaction {
id: string;
entryNumber: string;
entryDate: string;
description: string;
status: string;
totalAmount: number;
entities: Array<{ id: string; name: string; code: string }>;
lineCount: number;
lines: Array<{
id: string;
entityId: string | null;
entityName: string | null;
entityCode: string | null;
accountCode: string;
accountName: string;
debit: number;
credit: number;
description: string | null;
}>;
}
interface ICPosition {
fromEntity: { id: string; name: string; code: string };
toEntity: { id: string; name: string; code: string };
amount: number;
}
interface Summary {
totalEntities: number;
totalIntercompanyTransactions: number;
totalIntercompanyValue: number;
outstandingPositions: number;
totalOutstandingAmount: number;
}
export default function IntercompanyPage() {
const [entities, setEntities] = useState<Entity[]>([]);
const [transactions, setTransactions] = useState<ICTransaction[]>([]);
const [positions, setPositions] = useState<ICPosition[]>([]);
const [summary, setSummary] = useState<Summary | null>(null);
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState<'positions' | 'transactions'>('positions');
const [search, setSearch] = useState('');
const [expandedTxn, setExpandedTxn] = useState<string | null>(null);
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
setLoading(true);
try {
const res = await fetch('/api/intercompany');
const data = await res.json();
setEntities(data.entities || []);
setTransactions(data.transactions || []);
setPositions(data.positions || []);
setSummary(data.summary || null);
} catch (err) {
console.error('Failed to fetch intercompany data:', err);
} finally {
setLoading(false);
}
};
const statusColors: Record<string, string> = {
DRAFT: 'bg-[#1A1A1F] text-[#8B8B9E]',
PENDING: 'bg-amber-900/25 text-amber-700',
POSTED: 'bg-emerald-900/25 text-emerald-700',
APPROVED: 'bg-blue-900/25 text-blue-700',
REVERSED: 'bg-red-900/25 text-red-600',
};
const filteredTransactions = transactions.filter((txn) => {
if (!search.trim()) return true;
const q = search.toLowerCase();
return (
txn.entryNumber.toLowerCase().includes(q) ||
txn.description?.toLowerCase().includes(q) ||
txn.entities.some((e) => e.name.toLowerCase().includes(q) || e.code.toLowerCase().includes(q))
);
});
if (loading) {
return (
<div className="flex items-center justify-center min-h-[60vh]">
<Loader2 size={32} className="text-blue-500 animate-spin" />
</div>
);
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Link href="/finance/consolidation" className="p-2 hover:bg-[#222228] rounded-lg">
<ArrowLeft size={20} className="text-[#8B8B9E]" />
</Link>
<div>
<h1 className="text-2xl font-bold text-[#F0F0F3] flex items-center gap-2">
<GitMerge size={24} className="text-purple-600" />
Intercompany Transactions
</h1>
<p className="text-sm text-[#5A5A6E] mt-0.5">
Track and manage transactions between entities for elimination
</p>
</div>
</div>
<button
onClick={fetchData}
className="flex items-center gap-2 px-3 py-2 text-sm border border-[#2A2A32] rounded-lg hover:bg-[#222228]"
>
<RefreshCcw size={14} />
Refresh
</button>
</div>
{/* Summary Cards */}
{summary && (
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
<div className="bg-[#1A1A1F] rounded-xl p-4 border border-[#2A2A32]">
<p className="text-xs font-medium text-[#5A5A6E]">Entities</p>
<p className="text-2xl font-bold text-[#F0F0F3] mt-1">{summary.totalEntities}</p>
</div>
<div className="bg-purple-900/20 rounded-xl p-4 border border-purple-800/40">
<p className="text-xs font-medium text-purple-400">IC Transactions</p>
<p className="text-2xl font-bold text-purple-300 mt-1">{summary.totalIntercompanyTransactions}</p>
</div>
<div className="bg-blue-900/20 rounded-xl p-4 border border-blue-800/40" title={formatCurrency(summary.totalIntercompanyValue)}>
<p className="text-xs font-medium text-blue-400">Total IC Value</p>
<p className="text-2xl font-bold text-blue-300 mt-1">{formatCompactCurrency(summary.totalIntercompanyValue)}</p>
</div>
<div className="bg-amber-900/20 rounded-xl p-4 border border-amber-800/40">
<p className="text-xs font-medium text-amber-400">Open Positions</p>
<p className="text-2xl font-bold text-amber-300 mt-1">{summary.outstandingPositions}</p>
</div>
<div className="bg-red-900/20 rounded-xl p-4 border border-red-800/40" title={formatCurrency(summary.totalOutstandingAmount)}>
<p className="text-xs font-medium text-red-400">Outstanding Amount</p>
<p className="text-2xl font-bold text-red-300 mt-1">{formatCompactCurrency(summary.totalOutstandingAmount)}</p>
</div>
</div>
)}
{/* Tabs */}
<div className="bg-[#1A1A1F] rounded-xl border border-[#2A2A32] overflow-hidden">
<div className="flex items-center justify-between border-b border-[#2A2A32] px-4">
<div className="flex">
{[
{ id: 'positions' as const, label: 'IC Positions', icon: <Building2 size={14} /> },
{ id: 'transactions' as const, label: 'IC Journal Entries', icon: <FileText size={14} /> },
].map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium border-b-2 transition-colors ${
activeTab === tab.id
? 'border-purple-600 text-purple-400'
: 'border-transparent text-[#5A5A6E] hover:text-[#F0F0F3]'
}`}
>
{tab.icon} {tab.label}
</button>
))}
</div>
{activeTab === 'transactions' && (
<div className="flex items-center gap-2 bg-[#222228] rounded-lg px-3 py-1.5">
<Search size={14} className="text-[#5A5A6E]" />
<input
type="text"
placeholder="Search transactions..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="bg-transparent text-sm outline-none w-48"
/>
</div>
)}
</div>
{/* Positions Tab */}
{activeTab === 'positions' && (
<div className="p-4">
{positions.length === 0 ? (
<div className="text-center py-12">
<AlertCircle size={32} className="text-[#5A5A6E] mx-auto mb-2" />
<p className="text-sm text-[#5A5A6E]">No intercompany positions found</p>
<p className="text-xs text-[#5A5A6E] mt-1">
Positions appear when journal entries span multiple entities
</p>
</div>
) : (
<div className="space-y-3">
{positions.map((pos, idx) => (
<div
key={idx}
className="flex items-center gap-4 p-4 rounded-lg border border-[#2A2A32] hover:bg-[#222228] transition-colors"
>
{/* From Entity */}
<div className="flex-1">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-lg bg-blue-900/25 flex items-center justify-center">
<Building2 size={16} className="text-blue-600" />
</div>
<div>
<p className="text-sm font-semibold text-[#F0F0F3]">{pos.fromEntity.name}</p>
<p className="text-xs text-[#5A5A6E] font-mono">{pos.fromEntity.code}</p>
</div>
</div>
</div>
{/* Arrow + Amount */}
<div className="flex items-center gap-3 px-4">
<div className="text-right">
<p className="text-lg font-bold text-purple-400">{formatCurrency(pos.amount)}</p>
<p className="text-[10px] text-[#5A5A6E] uppercase tracking-wider">Owes</p>
</div>
<ArrowRight size={20} className="text-purple-400" />
</div>
{/* To Entity */}
<div className="flex-1">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-lg bg-emerald-900/25 flex items-center justify-center">
<Building2 size={16} className="text-emerald-600" />
</div>
<div>
<p className="text-sm font-semibold text-[#F0F0F3]">{pos.toEntity.name}</p>
<p className="text-xs text-[#5A5A6E] font-mono">{pos.toEntity.code}</p>
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
)}
{/* Transactions Tab */}
{activeTab === 'transactions' && (
<div>
{filteredTransactions.length === 0 ? (
<div className="text-center py-12">
<FileText size={32} className="text-[#5A5A6E] mx-auto mb-2" />
<p className="text-sm text-[#5A5A6E]">No intercompany journal entries found</p>
</div>
) : (
<div className="divide-y divide-[#2A2A32]">
{filteredTransactions.map((txn) => (
<div key={txn.id}>
<button
onClick={() => setExpandedTxn(expandedTxn === txn.id ? null : txn.id)}
className="w-full flex items-center gap-4 px-5 py-4 text-left hover:bg-[#222228] transition-colors"
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<Link
href={`/finance/general-ledger/entry/${txn.id}`}
onClick={(e) => e.stopPropagation()}
className="text-sm font-semibold text-blue-600 hover:underline"
>
{txn.entryNumber}
</Link>
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${statusColors[txn.status] || 'bg-[#1A1A1F] text-[#8B8B9E]'}`}>
{txn.status}
</span>
</div>
<p className="text-sm text-[#8B8B9E] truncate mt-0.5">{txn.description}</p>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
{txn.entities.map((e) => (
<span
key={e.id}
className="text-xs px-2 py-1 bg-purple-900/15 text-purple-700 rounded-md font-medium"
>
{e.code}
</span>
))}
</div>
<div className="text-right flex-shrink-0 w-32">
<p className="text-sm font-semibold text-[#F0F0F3]">{formatCurrency(txn.totalAmount)}</p>
<p className="text-xs text-[#5A5A6E]">
{new Date(txn.entryDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
</p>
</div>
</button>
{/* Expanded Lines */}
{expandedTxn === txn.id && (
<div className="px-5 pb-4">
<table className="w-full text-xs">
<thead>
<tr className="bg-[#111114]">
<th className="text-left px-3 py-2 font-semibold text-[#5A5A6E]">Entity</th>
<th className="text-left px-3 py-2 font-semibold text-[#5A5A6E]">Account</th>
<th className="text-left px-3 py-2 font-semibold text-[#5A5A6E]">Description</th>
<th className="text-right px-3 py-2 font-semibold text-[#5A5A6E]">Debit</th>
<th className="text-right px-3 py-2 font-semibold text-[#5A5A6E]">Credit</th>
</tr>
</thead>
<tbody className="divide-y divide-[#2A2A32]">
{txn.lines.map((line) => (
<tr key={line.id} className="hover:bg-[#222228]">
<td className="px-3 py-2">
<span className="px-1.5 py-0.5 bg-purple-900/15 text-purple-700 rounded font-medium">
{line.entityCode || '—'}
</span>
</td>
<td className="px-3 py-2 text-[#8B8B9E]">
<span className="font-mono">{line.accountCode}</span> {line.accountName}
</td>
<td className="px-3 py-2 text-[#5A5A6E]">{line.description || '—'}</td>
<td className="px-3 py-2 text-right font-semibold text-[#F0F0F3]">
{line.debit > 0 ? formatCurrency(line.debit) : ''}
</td>
<td className="px-3 py-2 text-right font-semibold text-[#F0F0F3]">
{line.credit > 0 ? formatCurrency(line.credit) : ''}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
))}
</div>
)}
</div>
)}
</div>
{/* Entity Overview */}
<div className="bg-[#1A1A1F] rounded-xl border border-[#2A2A32] overflow-hidden">
<div className="px-5 py-4 border-b border-[#2A2A32]">
<h2 className="text-lg font-semibold text-[#F0F0F3]">Entity Structure</h2>
<p className="text-xs text-[#5A5A6E] mt-0.5">{entities.length} entities in organization</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 p-4">
{entities.map((entity) => (
<div key={entity.id} className="p-4 rounded-lg border border-[#2A2A32] hover:border-purple-600 transition-colors">
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${
entity.type === 'PARENT' ? 'bg-[#222228]' : 'bg-[#222228]'
}`}>
<Building2 size={18} className={entity.type === 'PARENT' ? 'text-purple-600' : 'text-blue-600'} />
</div>
<div>
<p className="text-sm font-semibold text-[#F0F0F3]">{entity.name}</p>
<div className="flex items-center gap-2 mt-0.5">
<span className="text-xs text-[#5A5A6E] font-mono">{entity.code}</span>
<span className="text-xs px-1.5 py-0.5 bg-[#222228] text-[#5A5A6E] rounded">
{entity.type}
</span>
<span className="text-xs text-[#5A5A6E]">{entity.defaultCurrency}</span>
</div>
</div>
</div>
</div>
))}
{entities.length === 0 && (
<div className="col-span-3 text-center py-8">
<p className="text-sm text-[#5A5A6E]">No entities configured</p>
</div>
)}
</div>
</div>
</div>
);
}