Refactor Financials page: Add Settlements Table, Transaction Details, and Visual Upgrades
This commit is contained in:
116
src/features/financials/components/SettlementTable.tsx
Normal file
116
src/features/financials/components/SettlementTable.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
114
src/features/financials/components/TransactionList.tsx
Normal file
114
src/features/financials/components/TransactionList.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user