849 lines
35 KiB
TypeScript
849 lines
35 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { formatCurrency } from '@/lib/utils';
|
|
import Link from 'next/link';
|
|
import { useRouter } from 'next/navigation';
|
|
import {
|
|
ArrowLeft,
|
|
Loader2,
|
|
TrendingUp,
|
|
TrendingDown,
|
|
AlertCircle,
|
|
Edit2,
|
|
Download,
|
|
Plus,
|
|
Trash2,
|
|
} from 'lucide-react';
|
|
|
|
interface Account {
|
|
id: string;
|
|
accountCode: string;
|
|
accountName: string;
|
|
accountType: string;
|
|
isActive: boolean;
|
|
level: number;
|
|
description: string;
|
|
parentAccount: string | null;
|
|
}
|
|
|
|
interface Transaction {
|
|
id: string;
|
|
journalEntryId: string;
|
|
entryNumber: string;
|
|
entryDate: string;
|
|
status: string;
|
|
description: string;
|
|
entityName: string;
|
|
sourceModule?: string;
|
|
debitAmount: number;
|
|
creditAmount: number;
|
|
}
|
|
|
|
interface SubAccount {
|
|
id: string;
|
|
accountCode: string;
|
|
accountName: string;
|
|
accountType: string;
|
|
balance: number;
|
|
}
|
|
|
|
interface MonthlyData {
|
|
month: string;
|
|
debits: number;
|
|
credits: number;
|
|
balance: number;
|
|
}
|
|
|
|
interface AccountDetailData {
|
|
account: Account;
|
|
balance: number;
|
|
totalDebits: number;
|
|
totalCredits: number;
|
|
transactions: Transaction[];
|
|
subAccounts: SubAccount[];
|
|
monthlyData: MonthlyData[];
|
|
}
|
|
|
|
export default function AccountDetailPage({
|
|
params,
|
|
}: {
|
|
params: { accountId: string };
|
|
}) {
|
|
const router = useRouter();
|
|
const [data, setData] = useState<AccountDetailData | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [activeTab, setActiveTab] = useState<'transactions' | 'sub-accounts' | 'analysis' | 'audit'>('transactions');
|
|
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
|
|
|
|
// Edit Account State
|
|
const [isEditing, setIsEditing] = useState(false);
|
|
const [editForm, setEditForm] = useState({ name: '', description: '', status: '' });
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
|
|
// Delete Account State
|
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
|
const [isDeleting, setIsDeleting] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const fetchAccountData = async () => {
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
const response = await fetch(`/api/accounts/${params.accountId}`);
|
|
if (!response.ok) {
|
|
throw new Error('Failed to fetch account data');
|
|
}
|
|
const result = await response.json();
|
|
// Transform API response to match expected shape
|
|
const transformed: AccountDetailData = {
|
|
account: {
|
|
id: result.id,
|
|
accountCode: result.accountCode,
|
|
accountName: result.accountName,
|
|
accountType: result.accountType,
|
|
isActive: result.status === 'ACTIVE',
|
|
level: result.level || 1,
|
|
description: result.description || '',
|
|
parentAccount: result.parentAccount?.accountName || null,
|
|
},
|
|
balance: result.balance ?? 0,
|
|
totalDebits: result.totalDebits ?? 0,
|
|
totalCredits: result.totalCredits ?? 0,
|
|
transactions: (result.transactions || []).map((t: any) => ({
|
|
id: t.id,
|
|
journalEntryId: t.journalEntryId || t.journalEntry?.id,
|
|
entryNumber: t.journalEntry?.entryNumber || t.entryNumber || '',
|
|
entryDate: t.journalEntry?.entryDate || t.entryDate || '',
|
|
status: t.journalEntry?.status || t.status || '',
|
|
description: t.description || t.journalEntry?.description || '',
|
|
entityName: t.entity?.name || t.entityName || '',
|
|
debitAmount: t.debitAmount ?? 0,
|
|
creditAmount: t.creditAmount ?? 0,
|
|
})),
|
|
subAccounts: (result.subAccounts || []).map((s: any) => ({
|
|
id: s.id,
|
|
accountCode: s.accountCode,
|
|
accountName: s.accountName,
|
|
accountType: s.accountType,
|
|
balance: s.balance ?? 0,
|
|
})),
|
|
monthlyData: result.monthlyData
|
|
? (typeof result.monthlyData === 'object' && !Array.isArray(result.monthlyData)
|
|
? Object.entries(result.monthlyData)
|
|
.map(([month, data]: [string, any]) => ({
|
|
month,
|
|
debits: data.debits ?? 0,
|
|
credits: data.credits ?? 0,
|
|
balance: data.balance ?? 0,
|
|
}))
|
|
.sort((a, b) => a.month.localeCompare(b.month))
|
|
: result.monthlyData)
|
|
: [],
|
|
};
|
|
setData(transformed);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'An error occurred');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchAccountData();
|
|
}, [params.accountId]);
|
|
|
|
const handleEditClick = () => {
|
|
if (data) {
|
|
setEditForm({
|
|
name: data.account.accountName,
|
|
description: data.account.description,
|
|
status: data.account.isActive ? 'ACTIVE' : 'INACTIVE',
|
|
});
|
|
setIsEditing(true);
|
|
}
|
|
};
|
|
|
|
const handleEditCancel = () => {
|
|
setIsEditing(false);
|
|
setEditForm({ name: '', description: '', status: '' });
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
if (!data) return;
|
|
try {
|
|
setIsSaving(true);
|
|
const res = await fetch(`/api/accounts`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
action: 'update',
|
|
id: data.account.id,
|
|
accountName: editForm.name,
|
|
description: editForm.description,
|
|
status: editForm.status,
|
|
}),
|
|
});
|
|
if (res.ok) {
|
|
// Refetch account data
|
|
const response = await fetch(`/api/accounts/${params.accountId}`);
|
|
if (response.ok) {
|
|
const result = await response.json();
|
|
const transformed: AccountDetailData = {
|
|
account: {
|
|
id: result.id,
|
|
accountCode: result.accountCode,
|
|
accountName: result.accountName,
|
|
accountType: result.accountType,
|
|
isActive: result.status === 'ACTIVE',
|
|
level: result.level || 1,
|
|
description: result.description || '',
|
|
parentAccount: result.parentAccount?.accountName || null,
|
|
},
|
|
balance: result.balance ?? 0,
|
|
totalDebits: result.totalDebits ?? 0,
|
|
totalCredits: result.totalCredits ?? 0,
|
|
transactions: (result.transactions || []).map((t: any) => ({
|
|
id: t.id,
|
|
journalEntryId: t.journalEntryId || t.journalEntry?.id,
|
|
entryNumber: t.journalEntry?.entryNumber || t.entryNumber || '',
|
|
entryDate: t.journalEntry?.entryDate || t.entryDate || '',
|
|
status: t.journalEntry?.status || t.status || '',
|
|
description: t.description || t.journalEntry?.description || '',
|
|
entityName: t.entity?.name || t.entityName || '',
|
|
debitAmount: t.debitAmount ?? 0,
|
|
creditAmount: t.creditAmount ?? 0,
|
|
})),
|
|
subAccounts: (result.subAccounts || []).map((s: any) => ({
|
|
id: s.id,
|
|
accountCode: s.accountCode,
|
|
accountName: s.accountName,
|
|
accountType: s.accountType,
|
|
balance: s.balance ?? 0,
|
|
})),
|
|
monthlyData: result.monthlyData
|
|
? (typeof result.monthlyData === 'object' && !Array.isArray(result.monthlyData)
|
|
? Object.entries(result.monthlyData)
|
|
.map(([month, monthData]: [string, any]) => ({
|
|
month,
|
|
debits: monthData.debits ?? 0,
|
|
credits: monthData.credits ?? 0,
|
|
balance: monthData.balance ?? 0,
|
|
}))
|
|
.sort((a, b) => a.month.localeCompare(b.month))
|
|
: result.monthlyData)
|
|
: [],
|
|
};
|
|
setData(transformed);
|
|
}
|
|
setIsEditing(false);
|
|
setEditForm({ name: '', description: '', status: '' });
|
|
}
|
|
} catch (err) {
|
|
console.error('Error saving account:', err);
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
};
|
|
|
|
const handleStatusToggle = async () => {
|
|
if (!data) return;
|
|
try {
|
|
setIsSaving(true);
|
|
const newStatus = data.account.isActive ? 'INACTIVE' : 'ACTIVE';
|
|
const res = await fetch(`/api/accounts`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
action: 'update',
|
|
id: data.account.id,
|
|
status: newStatus,
|
|
}),
|
|
});
|
|
if (res.ok) {
|
|
setData({
|
|
...data,
|
|
account: { ...data.account, isActive: newStatus === 'ACTIVE' },
|
|
});
|
|
}
|
|
} catch (err) {
|
|
console.error('Error toggling status:', err);
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
};
|
|
|
|
const handleExport = () => {
|
|
if (!data) return;
|
|
const headers = ['Date', 'Entry #', 'Description', 'Entity', 'Source', 'Debit', 'Credit'];
|
|
const rows = data.transactions.map((tx) => [
|
|
new Date(tx.entryDate).toLocaleDateString('en-US'),
|
|
tx.entryNumber,
|
|
tx.description,
|
|
tx.entityName,
|
|
tx.sourceModule || 'Manual',
|
|
tx.debitAmount > 0 ? tx.debitAmount.toFixed(2) : '',
|
|
tx.creditAmount > 0 ? tx.creditAmount.toFixed(2) : '',
|
|
]);
|
|
|
|
const csv = [
|
|
headers.join(','),
|
|
...rows.map((row) => row.map((cell) => `"${cell}"`).join(',')),
|
|
].join('\n');
|
|
|
|
const blob = new Blob([csv], { type: 'text/csv' });
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `${data.account.accountCode}-transactions.csv`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
window.URL.revokeObjectURL(url);
|
|
};
|
|
|
|
const handleDelete = async () => {
|
|
if (!data) return;
|
|
try {
|
|
setIsDeleting(true);
|
|
const res = await fetch(`/api/accounts`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
action: 'delete',
|
|
id: data.account.id,
|
|
}),
|
|
});
|
|
if (res.ok) {
|
|
router.push('/finance/general-ledger');
|
|
}
|
|
} catch (err) {
|
|
console.error('Error deleting account:', err);
|
|
} finally {
|
|
setIsDeleting(false);
|
|
setShowDeleteConfirm(false);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen bg-[#111114] flex items-center justify-center">
|
|
<div className="flex flex-col items-center gap-4">
|
|
<Loader2 className="w-8 h-8 text-blue-600 animate-spin" />
|
|
<p className="text-[#8B8B9E]">Loading account details...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error || !data) {
|
|
return (
|
|
<div className="min-h-screen bg-[#111114]">
|
|
<div className="max-w-7xl mx-auto px-6 py-8">
|
|
<Link
|
|
href="/finance/general-ledger"
|
|
className="flex items-center gap-2 text-blue-600 hover:text-blue-700 mb-6"
|
|
>
|
|
<ArrowLeft className="w-4 h-4" />
|
|
Back to General Ledger
|
|
</Link>
|
|
<div className="bg-[#1A1A1F] rounded-lg border border-red-200 p-6 flex items-start gap-4">
|
|
<AlertCircle className="w-6 h-6 text-red-600 flex-shrink-0 mt-0.5" />
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-[#F0F0F3] mb-1">Error Loading Account</h2>
|
|
<p className="text-[#8B8B9E]">{error || 'Account not found'}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const { account, balance, totalDebits, totalCredits, transactions, subAccounts, monthlyData } = data;
|
|
|
|
const sortedTransactions = [...transactions].sort((a, b) => {
|
|
const dateA = new Date(a.entryDate).getTime();
|
|
const dateB = new Date(b.entryDate).getTime();
|
|
return sortOrder === 'desc' ? dateB - dateA : dateA - dateB;
|
|
});
|
|
|
|
const getAccountTypeBadge = (type: string) => {
|
|
const colors: Record<string, { bg: string; text: string }> = {
|
|
ASSET: { bg: 'bg-blue-900/25', text: 'text-blue-700' },
|
|
LIABILITY: { bg: 'bg-red-900/25', text: 'text-red-700' },
|
|
EQUITY: { bg: 'bg-purple-900/25', text: 'text-purple-700' },
|
|
REVENUE: { bg: 'bg-green-900/25', text: 'text-green-700' },
|
|
EXPENSE: { bg: 'bg-orange-900/25', text: 'text-orange-700' },
|
|
};
|
|
const color = colors[type] || { bg: 'bg-[#1A1A1F]', text: 'text-[#8B8B9E]' };
|
|
return (
|
|
<span className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${color.bg} ${color.text}`}>
|
|
{type}
|
|
</span>
|
|
);
|
|
};
|
|
|
|
const getMaxMonthlyValue = () => {
|
|
return Math.max(...monthlyData.map(m => Math.max(m.debits, m.credits, Math.abs(m.balance))), 1);
|
|
};
|
|
|
|
const maxValue = getMaxMonthlyValue();
|
|
|
|
return (
|
|
<div className="min-h-screen bg-[#111114]">
|
|
{/* Header */}
|
|
<div className="bg-[#1A1A1F] border-b border-[#2A2A32] sticky top-0 z-40">
|
|
<div className="max-w-7xl mx-auto px-6 py-4">
|
|
<div className="flex items-center justify-between gap-4 mb-4">
|
|
<div className="flex items-center gap-4">
|
|
<Link
|
|
href="/finance/general-ledger"
|
|
className="p-2 hover:bg-[#1A1A1F] rounded-lg transition-colors"
|
|
>
|
|
<ArrowLeft className="w-5 h-5 text-[#8B8B9E]" />
|
|
</Link>
|
|
<div>
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<h1 className="text-2xl font-bold text-[#F0F0F3]">
|
|
{account.accountCode}
|
|
</h1>
|
|
{getAccountTypeBadge(account.accountType)}
|
|
</div>
|
|
<p className="text-[#8B8B9E]">{account.accountName}</p>
|
|
</div>
|
|
</div>
|
|
{!isEditing && (
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={handleEditClick}
|
|
className="flex items-center gap-2 px-3 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm font-medium"
|
|
>
|
|
<Edit2 className="w-4 h-4" />
|
|
Edit
|
|
</button>
|
|
<button
|
|
onClick={handleExport}
|
|
className="flex items-center gap-2 px-3 py-2 bg-[#2A2A32] text-[#8B8B9E] rounded-lg hover:bg-[#333340] transition-colors text-sm font-medium"
|
|
>
|
|
<Download className="w-4 h-4" />
|
|
Export
|
|
</button>
|
|
<Link
|
|
href={`/finance/general-ledger/new-entry?accountId=${params.accountId}`}
|
|
className="flex items-center gap-2 px-3 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors text-sm font-medium"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
New Entry
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center justify-between gap-6 text-sm">
|
|
<div>
|
|
<p className="text-xs text-[#5A5A6E] uppercase tracking-wide">Status</p>
|
|
<p className="text-[#F0F0F3] font-medium mt-1">
|
|
{account.isActive ? (
|
|
<span className="text-green-600">Active</span>
|
|
) : (
|
|
<span className="text-red-600">Inactive</span>
|
|
)}
|
|
</p>
|
|
</div>
|
|
{!isEditing && (
|
|
<button
|
|
onClick={handleStatusToggle}
|
|
disabled={isSaving}
|
|
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
|
account.isActive
|
|
? 'bg-red-900/25 text-red-700 hover:bg-red-900/40'
|
|
: 'bg-green-900/25 text-green-700 hover:bg-green-900/40'
|
|
} disabled:opacity-50`}
|
|
>
|
|
{account.isActive ? 'Deactivate' : 'Activate'}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Edit Mode Header */}
|
|
{isEditing && (
|
|
<div className="bg-blue-900/15 border-b border-blue-200">
|
|
<div className="max-w-7xl mx-auto px-6 py-4">
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-[#8B8B9E] mb-1">Account Name</label>
|
|
<input
|
|
type="text"
|
|
value={editForm.name}
|
|
onChange={(e) => setEditForm({ ...editForm, name: e.target.value })}
|
|
className="w-full px-3 py-2 border border-[#3A3A45] rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-[#8B8B9E] mb-1">Description</label>
|
|
<textarea
|
|
value={editForm.description}
|
|
onChange={(e) => setEditForm({ ...editForm, description: e.target.value })}
|
|
rows={2}
|
|
className="w-full px-3 py-2 border border-[#3A3A45] rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-[#8B8B9E] mb-1">Status</label>
|
|
<select
|
|
value={editForm.status}
|
|
onChange={(e) => setEditForm({ ...editForm, status: e.target.value })}
|
|
className="w-full px-3 py-2 border border-[#3A3A45] rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-transparent"
|
|
>
|
|
<option value="ACTIVE">Active</option>
|
|
<option value="INACTIVE">Inactive</option>
|
|
</select>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={handleSave}
|
|
disabled={isSaving}
|
|
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors font-medium disabled:opacity-50"
|
|
>
|
|
{isSaving ? 'Saving...' : 'Save'}
|
|
</button>
|
|
<button
|
|
onClick={handleEditCancel}
|
|
disabled={isSaving}
|
|
className="px-4 py-2 bg-gray-300 text-[#8B8B9E] rounded-lg hover:bg-gray-400 transition-colors font-medium disabled:opacity-50"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Main Content */}
|
|
<div className="max-w-7xl mx-auto px-6 py-8">
|
|
{/* Summary Cards */}
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
|
<div className="bg-[#1A1A1F] rounded-lg border border-[#2A2A32] p-6">
|
|
<p className="text-xs font-semibold text-[#5A5A6E] uppercase tracking-wide">Current Balance</p>
|
|
<p className="text-2xl font-bold text-[#F0F0F3] mt-2">
|
|
{formatCurrency(balance)}
|
|
</p>
|
|
{balance >= 0 ? (
|
|
<div className="flex items-center gap-1 mt-2 text-green-600 text-sm">
|
|
<TrendingUp className="w-4 h-4" />
|
|
Positive
|
|
</div>
|
|
) : (
|
|
<div className="flex items-center gap-1 mt-2 text-red-600 text-sm">
|
|
<TrendingDown className="w-4 h-4" />
|
|
Negative
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="bg-[#1A1A1F] rounded-lg border border-[#2A2A32] p-6">
|
|
<p className="text-xs font-semibold text-[#5A5A6E] uppercase tracking-wide">Total Debits</p>
|
|
<p className="text-2xl font-bold text-[#F0F0F3] mt-2">
|
|
{formatCurrency(totalDebits)}
|
|
</p>
|
|
<p className="text-sm text-[#8B8B9E] mt-2">All time</p>
|
|
</div>
|
|
|
|
<div className="bg-[#1A1A1F] rounded-lg border border-[#2A2A32] p-6">
|
|
<p className="text-xs font-semibold text-[#5A5A6E] uppercase tracking-wide">Total Credits</p>
|
|
<p className="text-2xl font-bold text-[#F0F0F3] mt-2">
|
|
{formatCurrency(totalCredits)}
|
|
</p>
|
|
<p className="text-sm text-[#8B8B9E] mt-2">All time</p>
|
|
</div>
|
|
|
|
<div className="bg-[#1A1A1F] rounded-lg border border-[#2A2A32] p-6">
|
|
<p className="text-xs font-semibold text-[#5A5A6E] uppercase tracking-wide">Transactions</p>
|
|
<p className="text-2xl font-bold text-[#F0F0F3] mt-2">
|
|
{transactions.length}
|
|
</p>
|
|
<p className="text-sm text-[#8B8B9E] mt-2">Journal entries</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tabs */}
|
|
<div className="bg-[#1A1A1F] rounded-lg border border-[#2A2A32]">
|
|
<div className="border-b border-[#2A2A32]">
|
|
<div className="flex gap-8 px-6">
|
|
{(['transactions', 'sub-accounts', 'analysis', 'audit'] as const).map((tab) => (
|
|
<button
|
|
key={tab}
|
|
onClick={() => setActiveTab(tab)}
|
|
className={`py-4 px-0 font-medium text-sm border-b-2 transition-colors ${
|
|
activeTab === tab
|
|
? 'border-blue-600 text-blue-600'
|
|
: 'border-transparent text-[#8B8B9E] hover:text-[#F0F0F3]'
|
|
}`}
|
|
>
|
|
{tab === 'transactions' && 'Transactions'}
|
|
{tab === 'sub-accounts' && 'Sub-Accounts'}
|
|
{tab === 'analysis' && 'Analysis'}
|
|
{tab === 'audit' && 'Audit Trail'}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-6">
|
|
{/* Transactions Tab */}
|
|
{activeTab === 'transactions' && (
|
|
<div>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h2 className="text-lg font-semibold text-[#F0F0F3]">Journal Entry Transactions</h2>
|
|
<button
|
|
onClick={() => setSortOrder(sortOrder === 'desc' ? 'asc' : 'desc')}
|
|
className="text-sm text-[#8B8B9E] hover:text-[#F0F0F3]"
|
|
>
|
|
Sort: {sortOrder === 'desc' ? 'Newest First' : 'Oldest First'}
|
|
</button>
|
|
</div>
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead className="bg-[#111114] border-b border-[#2A2A32]">
|
|
<tr>
|
|
<th className="px-6 py-3 text-left text-xs font-semibold text-[#8B8B9E] uppercase tracking-wide">Date</th>
|
|
<th className="px-6 py-3 text-left text-xs font-semibold text-[#8B8B9E] uppercase tracking-wide">Entry #</th>
|
|
<th className="px-6 py-3 text-left text-xs font-semibold text-[#8B8B9E] uppercase tracking-wide">Description</th>
|
|
<th className="px-6 py-3 text-left text-xs font-semibold text-[#8B8B9E] uppercase tracking-wide">Entity</th>
|
|
<th className="px-6 py-3 text-left text-xs font-semibold text-[#8B8B9E] uppercase tracking-wide">Source</th>
|
|
<th className="px-6 py-3 text-right text-xs font-semibold text-[#8B8B9E] uppercase tracking-wide">Debit</th>
|
|
<th className="px-6 py-3 text-right text-xs font-semibold text-[#8B8B9E] uppercase tracking-wide">Credit</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{sortedTransactions.map((tx, idx) => (
|
|
<tr key={tx.id} className={idx % 2 === 0 ? 'bg-[#1A1A1F]' : 'bg-[#111114]'}>
|
|
<td className="px-6 py-4 text-sm text-[#8B8B9E] font-medium">
|
|
{new Date(tx.entryDate).toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
})}
|
|
</td>
|
|
<td className="px-6 py-4">
|
|
<Link
|
|
href={`/finance/general-ledger/entry/${tx.journalEntryId}`}
|
|
className="text-blue-600 hover:text-blue-700 hover:underline text-sm font-medium"
|
|
>
|
|
{tx.entryNumber}
|
|
</Link>
|
|
</td>
|
|
<td className="px-6 py-4 text-sm text-[#F0F0F3]">
|
|
{tx.description}
|
|
</td>
|
|
<td className="px-6 py-4 text-sm text-[#8B8B9E]">
|
|
{tx.entityName}
|
|
</td>
|
|
<td className="px-6 py-4 text-sm">
|
|
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-[#1A1A1F] text-[#8B8B9E]">
|
|
{tx.sourceModule || 'Manual'}
|
|
</span>
|
|
</td>
|
|
<td className="px-6 py-4 text-sm text-right font-medium">
|
|
{tx.debitAmount > 0 ? formatCurrency(tx.debitAmount) : '—'}
|
|
</td>
|
|
<td className="px-6 py-4 text-sm text-right font-medium">
|
|
{tx.creditAmount > 0 ? formatCurrency(tx.creditAmount) : '—'}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Sub-Accounts Tab */}
|
|
{activeTab === 'sub-accounts' && (
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-[#F0F0F3] mb-4">Sub-Accounts</h2>
|
|
{subAccounts.length === 0 ? (
|
|
<p className="text-[#8B8B9E]">No sub-accounts</p>
|
|
) : (
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead className="bg-[#111114] border-b border-[#2A2A32]">
|
|
<tr>
|
|
<th className="px-6 py-3 text-left text-xs font-semibold text-[#8B8B9E] uppercase tracking-wide">Code</th>
|
|
<th className="px-6 py-3 text-left text-xs font-semibold text-[#8B8B9E] uppercase tracking-wide">Name</th>
|
|
<th className="px-6 py-3 text-left text-xs font-semibold text-[#8B8B9E] uppercase tracking-wide">Type</th>
|
|
<th className="px-6 py-3 text-right text-xs font-semibold text-[#8B8B9E] uppercase tracking-wide">Balance</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{subAccounts.map((subAcc, idx) => (
|
|
<tr key={subAcc.id} className={idx % 2 === 0 ? 'bg-[#1A1A1F]' : 'bg-[#111114]'}>
|
|
<td className="px-6 py-4">
|
|
<Link
|
|
href={`/finance/general-ledger/account/${subAcc.accountCode}`}
|
|
className="text-blue-600 hover:text-blue-700 hover:underline text-sm font-medium"
|
|
>
|
|
{subAcc.accountCode}
|
|
</Link>
|
|
</td>
|
|
<td className="px-6 py-4 text-sm text-[#F0F0F3] font-medium">
|
|
{subAcc.accountName}
|
|
</td>
|
|
<td className="px-6 py-4 text-sm">
|
|
{getAccountTypeBadge(subAcc.accountType)}
|
|
</td>
|
|
<td className="px-6 py-4 text-sm text-right font-medium">
|
|
{formatCurrency(subAcc.balance)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Analysis Tab */}
|
|
{activeTab === 'analysis' && (
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-[#F0F0F3] mb-6">Monthly Trend Analysis</h2>
|
|
<div className="space-y-6">
|
|
{monthlyData.map((month) => (
|
|
<div key={month.month}>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<p className="text-sm font-medium text-[#F0F0F3]">
|
|
{new Date(month.month + '-01').toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
})}
|
|
</p>
|
|
<div className="flex gap-4 text-sm">
|
|
<div>
|
|
<span className="text-[#8B8B9E]">Debits: </span>
|
|
<span className="font-semibold text-[#F0F0F3]">{formatCurrency(month.debits)}</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-[#8B8B9E]">Credits: </span>
|
|
<span className="font-semibold text-[#F0F0F3]">{formatCurrency(month.credits)}</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-[#8B8B9E]">Balance: </span>
|
|
<span className={`font-semibold ${month.balance >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
|
{formatCurrency(month.balance)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-end gap-2 h-16 bg-[#111114] p-3 rounded-lg">
|
|
<div className="flex-1 flex items-end gap-1">
|
|
<div
|
|
className="flex-1 bg-blue-500 rounded-sm"
|
|
style={{ height: `${(month.debits / maxValue) * 100}%` }}
|
|
title={`Debits: ${formatCurrency(month.debits)}`}
|
|
/>
|
|
<div
|
|
className="flex-1 bg-red-500 rounded-sm"
|
|
style={{ height: `${(month.credits / maxValue) * 100}%` }}
|
|
title={`Credits: ${formatCurrency(month.credits)}`}
|
|
/>
|
|
<div
|
|
className={`flex-1 ${month.balance >= 0 ? 'bg-green-500' : 'bg-orange-500'} rounded-sm`}
|
|
style={{ height: `${(Math.abs(month.balance) / maxValue) * 100}%` }}
|
|
title={`Balance: ${formatCurrency(month.balance)}`}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="flex gap-6 mt-8 pt-6 border-t border-[#2A2A32]">
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-4 h-4 bg-blue-500 rounded-sm" />
|
|
<span className="text-sm text-[#8B8B9E]">Debits</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-4 h-4 bg-red-500 rounded-sm" />
|
|
<span className="text-sm text-[#8B8B9E]">Credits</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-4 h-4 bg-green-500 rounded-sm" />
|
|
<span className="text-sm text-[#8B8B9E]">Balance (Positive)</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Audit Trail Tab */}
|
|
{activeTab === 'audit' && (
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-[#F0F0F3] mb-4">Account Audit Trail</h2>
|
|
<div className="space-y-4">
|
|
<div className="bg-[#111114] p-4 rounded-lg">
|
|
<p className="text-sm text-[#8B8B9E] mb-1">
|
|
<span className="font-medium text-[#F0F0F3]">Account Created:</span> {account.accountCode}
|
|
</p>
|
|
<p className="text-xs text-[#5A5A6E]">
|
|
{account.accountName}
|
|
</p>
|
|
</div>
|
|
<p className="text-sm text-[#8B8B9E] text-center py-4">
|
|
Detailed audit trail coming soon
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Delete Confirmation Dialog */}
|
|
{showDeleteConfirm && (
|
|
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center">
|
|
<div className="bg-[#1A1A1F] rounded-lg shadow-lg max-w-md w-full mx-4 p-6">
|
|
<h3 className="text-lg font-semibold text-[#F0F0F3] mb-2">Delete Account</h3>
|
|
<p className="text-[#8B8B9E] mb-6">
|
|
Are you sure you want to delete this account? This action cannot be undone. All associated transaction records will remain for audit purposes.
|
|
</p>
|
|
<div className="flex items-center gap-3">
|
|
<button
|
|
onClick={handleDelete}
|
|
disabled={isDeleting}
|
|
className="flex-1 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors font-medium disabled:opacity-50"
|
|
>
|
|
{isDeleting ? 'Deleting...' : 'Delete Account'}
|
|
</button>
|
|
<button
|
|
onClick={() => setShowDeleteConfirm(false)}
|
|
disabled={isDeleting}
|
|
className="flex-1 px-4 py-2 bg-gray-300 text-[#8B8B9E] rounded-lg hover:bg-gray-400 transition-colors font-medium disabled:opacity-50"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Danger Zone */}
|
|
<div className="max-w-7xl mx-auto px-6 py-8">
|
|
<div className="bg-red-900/15 border border-red-200 rounded-lg p-6">
|
|
<h3 className="text-lg font-semibold text-red-900 mb-2">Danger Zone</h3>
|
|
<p className="text-red-700 text-sm mb-4">
|
|
Deleting this account will remove it from the chart of accounts. Existing transactions will be preserved for audit purposes.
|
|
</p>
|
|
<button
|
|
onClick={() => setShowDeleteConfirm(true)}
|
|
className="flex items-center gap-2 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors font-medium"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
Delete Account
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|