'use client' import { useState, useEffect } from 'react' interface Expense { id: number description: string amount: number currency_code: string amount_in_trip_currency: number category: string | null paid_by_name: string date_incurred: string created_at: string splits: Array<{ participant_name: string percentage: number amount: number is_settled: boolean }> } interface ParticipantBalance { name: string total_paid: number total_owed: number balance: number // positive = they are owed money, negative = they owe money } interface AnalyticsData { totalExpenses: number expensesByCategory: Array<{ category: string; amount: number; percentage: number }> expensesOverTime: Array<{ date: string; amount: number }> participantBalances: ParticipantBalance[] topExpenses: Expense[] } interface AnalyticsDashboardProps { tripId: number tripCurrency: string participants: Array<{ id: number; name: string; is_creator: boolean }> refreshTrigger?: number } const getCategoryIcon = (category: string | null) => { const icons: { [key: string]: string } = { 'Food & Dining': '🍔', 'Transportation': '🚗', 'Accommodation': '🏨', 'Entertainment': '🎬', 'Shopping': '🛍️', 'Healthcare': '🏥', 'Education': '📚', 'Business': '💼', 'Other': '📝' } return icons[category || 'Other'] || '📝' } const getCurrencySymbol = (currencyCode: string) => { const symbols: { [key: string]: string } = { 'USD': '$', 'EUR': '€', 'GBP': '£', 'JPY': '¥', 'CNY': '¥', 'INR': '₹', 'AED': 'د.إ', } return symbols[currencyCode] || currencyCode } const formatDate = (dateString: string) => { const date = new Date(dateString) return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) } export default function AnalyticsDashboard({ tripId, tripCurrency, participants, refreshTrigger }: AnalyticsDashboardProps) { const [analyticsData, setAnalyticsData] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState('') useEffect(() => { loadAnalyticsData() }, [tripId, refreshTrigger]) const loadAnalyticsData = async () => { try { setLoading(true) setError('') // Use relative URL to leverage Next.js API rewrite (works on mobile) const { getAuthHeaders } = await import('@/lib/trip-auth') const response = await fetch(`/api/expenses/${tripId}/expenses`, { headers: getAuthHeaders(tripId), }) if (!response.ok) { throw new Error('Failed to load expenses') } const expenses: Expense[] = await response.json() if (expenses.length === 0) { throw new Error('No expenses found') } // Calculate analytics data const totalExpenses = expenses.reduce((sum, expense) => sum + expense.amount_in_trip_currency, 0) // Group by category const categoryTotals: { [key: string]: number } = {} expenses.forEach(expense => { const category = expense.category || 'Other' categoryTotals[category] = (categoryTotals[category] || 0) + expense.amount_in_trip_currency }) const expensesByCategory = Object.entries(categoryTotals) .map(([category, amount]) => ({ category, amount, percentage: totalExpenses > 0 ? (amount / totalExpenses) * 100 : 0 })) .sort((a, b) => b.amount - a.amount) // Group by date for time series const dateTotals: { [key: string]: number } = {} expenses.forEach(expense => { const date = formatDate(expense.date_incurred) dateTotals[date] = (dateTotals[date] || 0) + expense.amount_in_trip_currency }) const expensesOverTime = Object.entries(dateTotals) .map(([date, amount]) => ({ date, amount })) .sort((a, b) => a.date.localeCompare(b.date)) // Calculate participant balances const participantBalances: ParticipantBalance[] = participants.map(participant => { let totalPaid = 0 let totalOwed = 0 expenses.forEach(expense => { // Check if this participant paid for the expense if (expense.paid_by_name === participant.name) { totalPaid += expense.amount_in_trip_currency } // Check if this participant owes for the expense const split = expense.splits.find(s => s.participant_name === participant.name) if (split) { totalOwed += split.amount } }) return { name: participant.name, total_paid: totalPaid, total_owed: totalOwed, balance: totalPaid - totalOwed // Positive = they are owed money } }) // Get top expenses const topExpenses = expenses .sort((a, b) => b.amount_in_trip_currency - a.amount_in_trip_currency) .slice(0, 5) setAnalyticsData({ totalExpenses, expensesByCategory, expensesOverTime, participantBalances, topExpenses }) } catch (err) { setError('Failed to load analytics data') console.error(err) } finally { setLoading(false) } } // Calculate settlements (who owes whom) const calculateSettlements = (balances: ParticipantBalance[]) => { const settlements: Array<{ from: string; to: string; amount: number }> = [] // Separate people who owe (negative balance) from people who are owed (positive balance) const debtors = balances.filter(b => b.balance < -0.01).map(b => ({ name: b.name, amount: Math.abs(b.balance) })) const creditors = balances.filter(b => b.balance > 0.01).map(b => ({ name: b.name, amount: b.balance })) // Sort by amount (largest first) debtors.sort((a, b) => b.amount - a.amount) creditors.sort((a, b) => b.amount - a.amount) // Match up debts with credits let debtorIdx = 0 let creditorIdx = 0 while (debtorIdx < debtors.length && creditorIdx < creditors.length) { const debtor = debtors[debtorIdx] const creditor = creditors[creditorIdx] // Calculate how much to transfer const transferAmount = Math.min(debtor.amount, creditor.amount) if (transferAmount > 0.01) { settlements.push({ from: debtor.name, to: creditor.name, amount: transferAmount }) } // Update remaining amounts debtor.amount -= transferAmount creditor.amount -= transferAmount if (debtor.amount < 0.01) { debtorIdx++ } if (creditor.amount < 0.01) { creditorIdx++ } } return settlements } if (loading) { return (
) } if (error) { return (

{error}

) } if (!analyticsData) { return null } const { totalExpenses, expensesByCategory, expensesOverTime, participantBalances, topExpenses } = analyticsData const settlements = calculateSettlements(participantBalances) return (
{/* Summary Card */}

Total Expenses

{getCurrencySymbol(tripCurrency)}{totalExpenses.toFixed(2)}

{expensesOverTime.length} day{expensesOverTime.length !== 1 ? 's' : ''} of tracking

{/* Expenses by Category */}

Expenses by Category

{expensesByCategory.map((item, index) => (
{getCategoryIcon(item.category)} {item.category}

{getCurrencySymbol(tripCurrency)}{item.amount.toFixed(2)}

{item.percentage.toFixed(1)}%

))}
{/* Settlements - Combined Who Owes Whom and Summary */}

Settlements

{/* Who Owes Whom Section */} {settlements.length > 0 ? ( <>

Who Owes Whom

{settlements.map((settlement, index) => (

{settlement.from} {settlement.to}

{settlement.from} should pay {settlement.to}

{getCurrencySymbol(tripCurrency)}{settlement.amount.toFixed(2)}

))}
) : (

✓ All participants are settled up!

)} {/* Participant Balances Summary */}

Participant Balances

{participantBalances .sort((a, b) => b.balance - a.balance) .map((balance, index) => (

{balance.name}

Paid: {getCurrencySymbol(tripCurrency)}{balance.total_paid.toFixed(2)} Owes: {getCurrencySymbol(tripCurrency)}{balance.total_owed.toFixed(2)}

0 ? 'text-green-600 dark:text-green-400' : balance.balance < 0 ? 'text-red-600 dark:text-red-400' : 'text-gray-600 dark:text-gray-400' }`}> {balance.balance > 0 ? '+' : ''}{getCurrencySymbol(tripCurrency)}{balance.balance.toFixed(2)}

{balance.balance > 0 ? 'gets back' : balance.balance < 0 ? 'owes' : 'settled'}

))}
{/* Top Expenses */} {topExpenses.length > 0 && (

Top Expenses

{topExpenses.map((expense, index) => (
{getCategoryIcon(expense.category)}

{expense.description}

{expense.paid_by_name} • {formatDate(expense.date_incurred)}

{getCurrencySymbol(expense.currency_code)}{expense.amount.toFixed(2)}

{expense.currency_code !== tripCurrency && (

{getCurrencySymbol(tripCurrency)}{expense.amount_in_trip_currency.toFixed(2)}

)}
))}
)} {/* Simple Time Series */} {expensesOverTime.length > 1 && (

Daily Spending

{expensesOverTime.slice(-7).map((item, index) => (
{item.date}
{getCurrencySymbol(tripCurrency)}{item.amount.toFixed(0)}
))}
)}
) }