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,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>
);
}