Refactor Financials page: Add Settlements Table, Transaction Details, and Visual Upgrades

This commit is contained in:
CycroftX
2026-02-03 20:55:29 +05:30
parent c616c260aa
commit 8b8cab9385
5 changed files with 488 additions and 70 deletions

View File

@@ -0,0 +1,120 @@
export interface Settlement {
id: string;
partnerName: string;
eventName: string;
amount: number;
dueDate: string;
status: 'Ready' | 'On Hold' | 'Overdue';
}
export interface Transaction {
id: string;
title: string;
partner: string;
amount: number;
date: string; // ISO string
type: 'in' | 'out';
method: 'Stripe' | 'Bank Transfer' | 'Razorpay';
fees: number;
net: number;
status: 'Completed' | 'Pending' | 'Failed';
}
export const mockSettlements: Settlement[] = [
{
id: 's1',
partnerName: 'Neon Arena',
eventName: 'Summer Music Festival',
amount: 125000,
dueDate: '2026-02-05',
status: 'Ready',
},
{
id: 's2',
partnerName: 'TopTier Promoters',
eventName: 'Comedy Night',
amount: 45000,
dueDate: '2026-02-06',
status: 'On Hold',
},
{
id: 's3',
partnerName: 'TechFlow Solutions',
eventName: 'Tech Summit 2026',
amount: 85000,
dueDate: '2026-02-02', // Past date
status: 'Overdue',
},
{
id: 's4',
partnerName: 'Global Sponsors Inc',
eventName: 'Corporate Gala',
amount: 250000,
dueDate: '2026-02-10',
status: 'Ready',
},
];
export const mockTransactions: Transaction[] = [
{
id: 't1',
title: 'Ticket Sales - Summer Fest',
partner: 'Neon Arena',
amount: 25000,
date: new Date().toISOString(),
type: 'in',
method: 'Razorpay',
fees: 1250,
net: 23750,
status: 'Completed',
},
{
id: 't2',
title: 'Payout - Neon Arena',
partner: 'Neon Arena',
amount: 15000,
date: new Date().toISOString(),
type: 'out',
method: 'Bank Transfer',
fees: 0,
net: 15000,
status: 'Completed',
},
{
id: 't3',
title: 'Ticket Sales - Comedy Night',
partner: 'TopTier Promoters',
amount: 4500,
date: new Date(Date.now() - 86400000).toISOString(), // Yesterday
type: 'in',
method: 'Stripe',
fees: 225,
net: 4275,
status: 'Completed',
},
{
id: 't4',
title: 'Refund - User #442',
partner: 'Neon Arena',
amount: 1500,
date: new Date(Date.now() - 86400000).toISOString(),
type: 'out',
method: 'Razorpay',
fees: 0,
net: 1500,
status: 'Completed',
},
{
id: 't5',
title: 'Ticket Sales - Tech Summit',
partner: 'TechFlow Solutions',
amount: 12000,
date: '2026-02-01T10:00:00Z',
type: 'in',
method: 'Razorpay',
fees: 600,
net: 11400,
status: 'Completed',
},
];

View File

@@ -0,0 +1,116 @@
import { useState } from "react";
import { format } from "date-fns";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from "@/components/ui/table";
import { Checkbox } from "@/components/ui/checkbox";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { mockSettlements, Settlement } from "@/data/mockFinancialData";
import { ArrowUpRight } from "lucide-react";
import { toast } from "sonner";
export function SettlementTable() {
const [selected, setSelected] = useState<string[]>([]);
const handleSelectAll = (checked: boolean) => {
if (checked) {
setSelected(mockSettlements.map(s => s.id));
} else {
setSelected([]);
}
};
const handleSelectOne = (id: string, checked: boolean) => {
if (checked) {
setSelected(prev => [...prev, id]);
} else {
setSelected(prev => prev.filter(item => item !== id));
}
};
const handleReleasePayout = () => {
toast.success(`Processing payouts for ${selected.length} partners`);
setSelected([]);
};
const getStatusBadge = (status: Settlement['status']) => {
switch (status) {
case 'Ready':
return <Badge className="bg-success/15 text-success hover:bg-success/25 border-none">Ready</Badge>;
case 'On Hold':
return <Badge className="bg-warning/15 text-warning hover:bg-warning/25 border-none">On Hold</Badge>;
case 'Overdue':
return <Badge className="bg-error/15 text-error hover:bg-error/25 border-none">Overdue</Badge>;
default: return null;
}
};
return (
<div className="neu-card p-6">
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-lg font-bold text-foreground">Due for Settlement</h2>
<p className="text-sm text-muted-foreground">Manage pending partner payouts</p>
</div>
<Button
onClick={handleReleasePayout}
disabled={selected.length === 0}
className="bg-primary hover:bg-primary/90 text-white"
>
Release Payout ({selected.length})
<ArrowUpRight className="ml-2 h-4 w-4" />
</Button>
</div>
<div className="rounded-md border border-border/50">
<Table>
<TableHeader>
<TableRow className="hover:bg-transparent">
<TableHead className="w-[50px]">
<Checkbox
checked={selected.length === mockSettlements.length && mockSettlements.length > 0}
onCheckedChange={(checked) => handleSelectAll(checked as boolean)}
/>
</TableHead>
<TableHead>Partner Name</TableHead>
<TableHead>Event</TableHead>
<TableHead>Unsettled Amount</TableHead>
<TableHead>Due Date</TableHead>
<TableHead className="text-right">Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{mockSettlements.map((settlement) => (
<TableRow key={settlement.id} className="hover:bg-secondary/20">
<TableCell>
<Checkbox
checked={selected.includes(settlement.id)}
onCheckedChange={(checked) => handleSelectOne(settlement.id, checked as boolean)}
/>
</TableCell>
<TableCell className="font-medium">{settlement.partnerName}</TableCell>
<TableCell className="text-muted-foreground">{settlement.eventName}</TableCell>
<TableCell className="font-semibold">
{settlement.amount.toLocaleString('en-IN', { style: 'currency', currency: 'INR' })}
</TableCell>
<TableCell>
{format(new Date(settlement.dueDate), 'MMM dd, yyyy')}
</TableCell>
<TableCell className="text-right">
{getStatusBadge(settlement.status)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
);
}

View File

@@ -0,0 +1,93 @@
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import { Transaction } from "@/data/mockFinancialData";
import { AlertCircle, CheckCircle2, XCircle } from "lucide-react";
interface TransactionDetailsSheetProps {
transaction: Transaction | null;
open: boolean;
onOpenChange: (open: boolean) => void;
}
export function TransactionDetailsSheet({ transaction, open, onOpenChange }: TransactionDetailsSheetProps) {
if (!transaction) return null;
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent>
<SheetHeader>
<SheetTitle>Transaction Details</SheetTitle>
<SheetDescription>
ID: {transaction.id}
</SheetDescription>
</SheetHeader>
<div className="mt-8 space-y-6">
<div className="flex items-center justify-between p-4 bg-secondary/30 rounded-xl">
<div className="flex items-center gap-3">
{transaction.status === 'Completed' ? (
<CheckCircle2 className="h-8 w-8 text-success" />
) : transaction.status === 'Failed' ? (
<XCircle className="h-8 w-8 text-error" />
) : (
<AlertCircle className="h-8 w-8 text-warning" />
)}
<div>
<p className="font-bold text-lg">{transaction.amount.toLocaleString('en-IN', { style: 'currency', currency: 'INR' })}</p>
<p className="text-sm text-muted-foreground">{transaction.status}</p>
</div>
</div>
<div className="text-right">
<p className="text-sm font-medium">{new Date(transaction.date).toLocaleDateString()}</p>
<p className="text-xs text-muted-foreground">{new Date(transaction.date).toLocaleTimeString()}</p>
</div>
</div>
<div className="space-y-4">
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wide">Breakdown</h3>
<div className="space-y-3">
<div className="flex justify-between">
<span className="text-foreground">Gross Amount</span>
<span className="font-medium">{transaction.amount.toLocaleString('en-IN', { style: 'currency', currency: 'INR' })}</span>
</div>
<div className="flex justify-between text-muted-foreground">
<span>Platform Fees (5%)</span>
<span>- {transaction.fees.toLocaleString('en-IN', { style: 'currency', currency: 'INR' })}</span>
</div>
<div className="h-px bg-border my-2" />
<div className="flex justify-between text-lg font-bold">
<span>Net Settlement</span>
<span className="text-success">{transaction.net.toLocaleString('en-IN', { style: 'currency', currency: 'INR' })}</span>
</div>
</div>
</div>
<div className="space-y-4">
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wide">Metadata</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-xs text-muted-foreground">Partner</p>
<p className="font-medium">{transaction.partner}</p>
</div>
<div>
<p className="text-xs text-muted-foreground">Method</p>
<p className="font-medium">{transaction.method}</p>
</div>
<div className="col-span-2">
<p className="text-xs text-muted-foreground">Description</p>
<p className="font-medium">{transaction.title}</p>
</div>
</div>
</div>
</div>
</SheetContent>
</Sheet>
);
}

View File

@@ -0,0 +1,114 @@
import { useState } from "react";
import { format, isToday, isYesterday } from "date-fns";
import { mockTransactions, Transaction } from "@/data/mockFinancialData";
import { TransactionDetailsSheet } from "./TransactionDetailsSheet";
import { ArrowUpRight, ArrowDownRight, CreditCard, Landmark, Wallet } from "lucide-react";
import { cn } from "@/lib/utils";
export function TransactionList() {
const [selectedTx, setSelectedTx] = useState<Transaction | null>(null);
const [open, setOpen] = useState(false);
// Group transactions by date
const groupedTransactions = mockTransactions.reduce((groups, tx) => {
const date = new Date(tx.date);
let key = format(date, 'yyyy-MM-dd');
if (isToday(date)) key = 'Today';
else if (isYesterday(date)) key = 'Yesterday';
else key = format(date, 'MMMM dd, yyyy');
if (!groups[key]) {
groups[key] = [];
}
groups[key].push(tx);
return groups;
}, {} as Record<string, Transaction[]>);
const handleRowClick = (tx: Transaction) => {
setSelectedTx(tx);
setOpen(true);
};
const getMethodIcon = (method: Transaction['method']) => {
switch (method) {
case 'Stripe': return <CreditCard className="h-4 w-4" />;
case 'Bank Transfer': return <Landmark className="h-4 w-4" />;
case 'Razorpay': return <Wallet className="h-4 w-4" />;
default: return <Wallet className="h-4 w-4" />;
}
};
return (
<div className="neu-card p-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-lg font-bold text-foreground">Recent Transactions</h2>
<button className="text-sm font-medium text-accent hover:underline">
View All
</button>
</div>
<div className="space-y-6">
{Object.entries(groupedTransactions).map(([date, transactions]) => (
<div key={date}>
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3 px-2">
{date}
</h3>
<div className="space-y-2">
{transactions.map((tx) => (
<div
key={tx.id}
onClick={() => handleRowClick(tx)}
className={cn(
"flex items-center justify-between p-3 rounded-lg cursor-pointer transition-all",
"hover:bg-secondary/50 border border-transparent hover:border-border/50",
tx.type === 'out' ? "bg-error/5 hover:bg-error/10" : "bg-card"
)}
>
<div className="flex items-center gap-4">
<div className={cn(
"h-10 w-10 rounded-full flex items-center justify-center shrink-0",
tx.type === 'in' ? "bg-success/10 text-success" : "bg-error/10 text-error"
)}>
{tx.type === 'in' ? <ArrowUpRight className="h-5 w-5" /> : <ArrowDownRight className="h-5 w-5" />}
</div>
<div className="min-w-[120px]">
<p className="font-medium text-sm text-foreground">{tx.title}</p>
<div className="flex items-center gap-2 text-xs text-muted-foreground mt-0.5">
<span className="flex items-center gap-1">
{getMethodIcon(tx.method)}
{tx.method}
</span>
<span></span>
<span>{format(new Date(tx.date), 'hh:mm a')}</span>
</div>
</div>
</div>
<div className="text-right">
<p className={cn(
"font-bold text-sm",
tx.type === 'in' ? "text-success" : "text-error"
)}>
{tx.type === 'in' ? '+' : '-'}{tx.amount.toLocaleString('en-IN', { style: 'currency', currency: 'INR' })}
</p>
<p className="text-xs text-muted-foreground">
{tx.status}
</p>
</div>
</div>
))}
</div>
</div>
))}
</div>
<TransactionDetailsSheet
transaction={selectedTx}
open={open}
onOpenChange={setOpen}
/>
</div>
);
}

View File

@@ -1,19 +1,23 @@
import { IndianRupee, TrendingUp, Wallet, ArrowUpRight, ArrowDownRight } from 'lucide-react';
import { IndianRupee, TrendingUp, Wallet } from 'lucide-react';
import { AppLayout } from '@/components/layout/AppLayout';
import { formatCurrency, mockRevenueData } from '@/data/mockData';
import { SettlementTable } from '@/features/financials/components/SettlementTable';
import { TransactionList } from '@/features/financials/components/TransactionList';
export default function Financials() {
const totalRevenue = mockRevenueData.reduce((sum, d) => sum + d.revenue, 0);
const totalPayouts = mockRevenueData.reduce((sum, d) => sum + d.payouts, 0);
const platformFee = totalRevenue - totalPayouts;
// Calculating platform fee (approx 10% of revenue for mock)
const platformEarnings = totalRevenue * 0.12;
return (
<AppLayout
title="Financials"
title="Financials & Settlements"
description="Track revenue, payouts, and platform earnings."
>
{/* Financial Overview */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
{/* Financial Overview Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div className="neu-card p-6">
<div className="flex items-center gap-4">
<div className="h-12 w-12 rounded-xl bg-success/10 flex items-center justify-center">
@@ -21,10 +25,11 @@ export default function Financials() {
</div>
<div>
<p className="text-2xl font-bold text-foreground">{formatCurrency(totalRevenue)}</p>
<p className="text-sm text-muted-foreground">Total Revenue (7d)</p>
<p className="text-sm text-muted-foreground">Total Revenue (All Time)</p>
</div>
</div>
</div>
<div className="neu-card p-6">
<div className="flex items-center gap-4">
<div className="h-12 w-12 rounded-xl bg-accent/10 flex items-center justify-center">
@@ -32,82 +37,52 @@ export default function Financials() {
</div>
<div>
<p className="text-2xl font-bold text-foreground">{formatCurrency(totalPayouts)}</p>
<p className="text-sm text-muted-foreground">Partner Payouts (7d)</p>
<p className="text-sm text-muted-foreground">Partner Payouts (Processed)</p>
</div>
</div>
</div>
<div className="neu-card p-6">
<div className="flex items-center gap-4">
<div className="h-12 w-12 rounded-xl bg-royal-blue/10 flex items-center justify-center">
<TrendingUp className="h-6 w-6 text-royal-blue" />
{/* Premium Platform Earnings Card */}
<div className="relative overflow-hidden rounded-xl border border-yellow-500/20 bg-gradient-to-br from-yellow-500/10 to-orange-500/10 p-6 shadow-neu">
<div className="flex items-center gap-4 relative z-10">
<div className="h-12 w-12 rounded-xl bg-yellow-500/20 flex items-center justify-center border border-yellow-500/30">
<TrendingUp className="h-6 w-6 text-yellow-600 dark:text-yellow-400" />
</div>
<div>
<p className="text-2xl font-bold text-foreground">{formatCurrency(platformFee)}</p>
<p className="text-sm text-muted-foreground">Platform Earnings (7d)</p>
</div>
</div>
</div>
<div className="neu-card p-6">
<div className="flex items-center gap-4">
<div className="h-12 w-12 rounded-xl bg-warning/10 flex items-center justify-center">
<Wallet className="h-6 w-6 text-warning" />
</div>
<div>
<p className="text-2xl font-bold text-foreground">{formatCurrency(845000)}</p>
<p className="text-sm text-muted-foreground">Pending Payouts</p>
<p className="text-2xl font-bold text-foreground">{formatCurrency(platformEarnings)}</p>
<p className="text-sm text-yellow-700/80 dark:text-yellow-400/80 font-medium">Net Platform Earnings</p>
</div>
</div>
{/* Decorative background glow */}
<div className="absolute -right-6 -top-6 h-24 w-24 bg-yellow-500/20 blur-2xl rounded-full" />
</div>
</div>
{/* Transaction History */}
<div className="neu-card p-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-lg font-bold text-foreground">Recent Transactions</h2>
<button className="text-sm font-medium text-accent hover:underline">
View All
</button>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Main Section: Payouts Command Center (2/3 width) */}
<div className="lg:col-span-2 space-y-8">
<SettlementTable />
{/* Detailed Transaction List */}
<TransactionList />
</div>
<div className="space-y-4">
{[
{ id: '1', type: 'in', title: 'Mumbai Music Festival', partner: 'Music Nights', amount: 489000, date: new Date() },
{ id: '2', type: 'out', title: 'Partner Payout', partner: 'TechConf India', amount: 245000, date: new Date(Date.now() - 1000 * 60 * 60 * 2) },
{ id: '3', type: 'in', title: 'Tech Summit 2024', partner: 'TechConf India', amount: 156000, date: new Date(Date.now() - 1000 * 60 * 60 * 5) },
{ id: '4', type: 'out', title: 'Partner Payout', partner: 'Music Nights', amount: 180000, date: new Date(Date.now() - 1000 * 60 * 60 * 8) },
{ id: '5', type: 'in', title: 'Night Club Party', partner: 'Music Nights', amount: 24000, date: new Date(Date.now() - 1000 * 60 * 60 * 12) },
].map((tx) => (
<div key={tx.id} className="flex items-center justify-between p-4 rounded-xl bg-secondary/30 hover:bg-secondary/50 transition-colors">
<div className="flex items-center gap-4">
<div className={`h-10 w-10 rounded-xl flex items-center justify-center ${
tx.type === 'in' ? 'bg-success/10' : 'bg-error/10'
}`}>
{tx.type === 'in' ? (
<ArrowUpRight className="h-5 w-5 text-success" />
) : (
<ArrowDownRight className="h-5 w-5 text-error" />
)}
</div>
<div>
<p className="font-medium text-foreground">{tx.title}</p>
<p className="text-sm text-muted-foreground">{tx.partner}</p>
</div>
</div>
<div className="text-right">
<p className={`font-bold ${tx.type === 'in' ? 'text-success' : 'text-error'}`}>
{tx.type === 'in' ? '+' : '-'}{formatCurrency(tx.amount)}
</p>
<p className="text-sm text-muted-foreground">
{tx.date.toLocaleDateString('en-IN', {
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit'
})}
</p>
</div>
{/* Sidebar Section: Summary or Quick Actions (1/3 width) */}
<div className="space-y-6">
{/* Could add mini charts or notifications here */}
<div className="neu-card p-6">
<h3 className="font-semibold mb-4">Quick Actions</h3>
<div className="space-y-2">
<button className="w-full text-left px-4 py-3 rounded-lg bg-secondary/50 hover:bg-secondary transition-colors text-sm font-medium flex items-center justify-between group">
Download Monthly Report
<IndianRupee className="h-4 w-4 text-muted-foreground group-hover:text-foreground" />
</button>
<button className="w-full text-left px-4 py-3 rounded-lg bg-secondary/50 hover:bg-secondary transition-colors text-sm font-medium flex items-center justify-between group">
Update Tax Settings
<Wallet className="h-4 w-4 text-muted-foreground group-hover:text-foreground" />
</button>
</div>
))}
</div>
</div>
</div>
</AppLayout>