Donation status badge upates, admin validation tootips

This commit is contained in:
2026-01-29 19:37:41 -06:00
parent de719d9d69
commit d5152609b6
4 changed files with 156 additions and 132 deletions

View File

@@ -6,7 +6,6 @@ const STATUS_BADGE_CONFIG = {
//status-based badges //status-based badges
pending_email: { label: 'Pending Email', variant: 'orange2' }, pending_email: { label: 'Pending Email', variant: 'orange2' },
pending_validation: { label: 'Pending Validation', variant: 'gray' }, pending_validation: { label: 'Pending Validation', variant: 'gray' },
pre_validated: { label: 'Pre-Validated', variant: 'green' },
payment_pending: { label: 'Payment Pending', variant: 'orange' }, payment_pending: { label: 'Payment Pending', variant: 'orange' },
active: { label: 'Active', variant: 'green' }, active: { label: 'Active', variant: 'green' },
inactive: { label: 'Inactive', variant: 'gray2' }, inactive: { label: 'Inactive', variant: 'gray2' },
@@ -23,7 +22,12 @@ const STATUS_BADGE_CONFIG = {
admin: { label: 'Admin', variant: 'purple' }, admin: { label: 'Admin', variant: 'purple' },
moderator: { label: 'Moderator', variant: 'bg-[var(--neutral-800)] text-[var(--purple-ink)]' }, moderator: { label: 'Moderator', variant: 'bg-[var(--neutral-800)] text-[var(--purple-ink)]' },
staff: { label: 'Staff', variant: 'gray' }, staff: { label: 'Staff', variant: 'gray' },
media: { label: 'Media', variant: 'gray2' } media: { label: 'Media', variant: 'gray2' },
//donation badges
pending: { label: 'Payment Pending', variant: 'orange' },
completed: { label: 'Completed', variant: 'green' },
failed: { label: 'Failed', className: 'bg-red-100 text-red-700' }
}; };
//todo: make shield icon dynamic based on status //todo: make shield icon dynamic based on status

View File

@@ -1,78 +1,91 @@
import * as React from "react" import * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const Table = React.forwardRef(({ className, ...props }, ref) => ( const Table = React.forwardRef(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto"> <div className="relative w-full overflow-auto">
<table <table ref={ref} className={cn("w-full", className)} {...props} />
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props} />
</div> </div>
)) ));
Table.displayName = "Table" Table.displayName = "Table";
const TableHeader = React.forwardRef(({ className, ...props }, ref) => ( const TableHeader = React.forwardRef(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} /> <thead
)) ref={ref}
TableHeader.displayName = "TableHeader" className={cn(
"bg-[var(--lavender-300)] border-b border-[var(--neutral-800)]",
className,
)}
{...props}
/>
));
TableHeader.displayName = "TableHeader";
const TableBody = React.forwardRef(({ className, ...props }, ref) => ( const TableBody = React.forwardRef(({ className, ...props }, ref) => (
<tbody <tbody
ref={ref} ref={ref}
className={cn("[&_tr:last-child]:border-0", className)} className={cn("[&_tr:last-child]:border-0", className)}
{...props} /> {...props}
)) />
TableBody.displayName = "TableBody" ));
TableBody.displayName = "TableBody";
const TableFooter = React.forwardRef(({ className, ...props }, ref) => ( const TableFooter = React.forwardRef(({ className, ...props }, ref) => (
<tfoot <tfoot
ref={ref} ref={ref}
className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)} className={cn(
{...props} /> "border-t border-[var(--neutral-800)] font-medium [&>tr]:last:border-b-0",
)) className,
TableFooter.displayName = "TableFooter" )}
{...props}
/>
));
TableFooter.displayName = "TableFooter";
const TableRow = React.forwardRef(({ className, ...props }, ref) => ( const TableRow = React.forwardRef(({ className, ...props }, ref) => (
<tr <tr
ref={ref} ref={ref}
className={cn( className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", "border-b border-[var(--neutral-800)] transition-colors hover:bg-[var(--lavender-400)]",
className className,
)} )}
{...props} /> {...props}
)) />
TableRow.displayName = "TableRow" ));
TableRow.displayName = "TableRow";
const TableHead = React.forwardRef(({ className, ...props }, ref) => ( const TableHead = React.forwardRef(({ className, ...props }, ref) => (
<th <th
ref={ref} ref={ref}
className={cn( className={cn(
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", "p-4 text-left align-middle font-semibold text-[var(--purple-ink)] [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className className,
)} )}
{...props} /> {...props}
)) />
TableHead.displayName = "TableHead" ));
TableHead.displayName = "TableHead";
const TableCell = React.forwardRef(({ className, ...props }, ref) => ( const TableCell = React.forwardRef(({ className, ...props }, ref) => (
<td <td
ref={ref} ref={ref}
className={cn( className={cn(
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", "p-4 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] ",
className className,
)} )}
{...props} /> {...props}
)) />
TableCell.displayName = "TableCell" ));
TableCell.displayName = "TableCell";
const TableCaption = React.forwardRef(({ className, ...props }, ref) => ( const TableCaption = React.forwardRef(({ className, ...props }, ref) => (
<caption <caption
ref={ref} ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)} className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props} /> {...props}
)) />
TableCaption.displayName = "TableCaption" ));
TableCaption.displayName = "TableCaption";
export { export {
Table, Table,
@@ -83,4 +96,4 @@ export {
TableRow, TableRow,
TableCell, TableCell,
TableCaption, TableCaption,
} };

View File

@@ -3,6 +3,7 @@ import { useAuth } from '../../context/AuthContext';
import { Card } from '../../components/ui/card'; import { Card } from '../../components/ui/card';
import { Button } from '../../components/ui/button'; import { Button } from '../../components/ui/button';
import { Input } from '../../components/ui/input'; import { Input } from '../../components/ui/input';
import StatusBadge from '@/components/StatusBadge';
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -17,6 +18,14 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from '../../components/ui/dropdown-menu'; } from '../../components/ui/dropdown-menu';
import { Badge } from '../../components/ui/badge'; import { Badge } from '../../components/ui/badge';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '../../components/ui/table';
import api from '../../utils/api'; import api from '../../utils/api';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { import {
@@ -184,15 +193,8 @@ const AdminDonations = () => {
toast.error('Failed to copy to clipboard'); toast.error('Failed to copy to clipboard');
} }
}; };
/*
const getStatusBadgeVariant = (status) => { */
const variants = {
completed: 'default',
pending: 'secondary',
failed: 'destructive'
};
return variants[status] || 'outline';
};
const getTypeBadgeColor = (type) => { const getTypeBadgeColor = (type) => {
return type === 'member' ? 'bg-[var(--green-light)]' : 'bg-brand-purple '; return type === 'member' ? 'bg-[var(--green-light)]' : 'bg-brand-purple ';
@@ -392,51 +394,37 @@ const AdminDonations = () => {
{/* Donations Table */} {/* Donations Table */}
<Card className="bg-background rounded-2xl border-2 border-[var(--neutral-800)] overflow-hidden"> <Card className="bg-background rounded-2xl border-2 border-[var(--neutral-800)] overflow-hidden">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full"> <Table>
<thead className="bg-[var(--lavender-300)] border-b-2 border-[var(--neutral-800)]"> <TableHeader>
<tr> <TableRow>
<th className="px-6 py-4 text-left text-sm font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}> <TableHead>Donor</TableHead>
Donor <TableHead>Type</TableHead>
</th> <TableHead>Amount</TableHead>
<th className="px-6 py-4 text-left text-sm font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}> <TableHead>Status</TableHead>
Type <TableHead>Date</TableHead>
</th> <TableHead>Payment Method</TableHead>
<th className="px-6 py-4 text-left text-sm font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}> <TableHead className="text-center">Details</TableHead>
Amount </TableRow>
</th> </TableHeader>
<th className="px-6 py-4 text-left text-sm font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}> <TableBody>
Status
</th>
<th className="px-6 py-4 text-left text-sm font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
Date
</th>
<th className="px-6 py-4 text-left text-sm font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
Payment Method
</th>
<th className="px-6 py-4 text-center text-sm font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
Details
</th>
</tr>
</thead>
<tbody className="divide-y divide-[var(--neutral-800)]">
{filteredDonations.length === 0 ? ( {filteredDonations.length === 0 ? (
<tr> <TableRow>
<td colSpan="7" className="px-6 py-12 text-center"> <TableCell colSpan={7} className="p-12 text-center">
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<Heart className="h-12 w-12 text-[var(--neutral-800)]" /> <Heart className="h-12 w-12 text-[var(--neutral-800)]" />
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{donations.length === 0 ? 'No donations yet' : 'No donations match your filters'} {donations.length === 0 ? 'No donations yet' : 'No donations match your filters'}
</p> </p>
</div> </div>
</td> </TableCell>
</tr> </TableRow>
) : ( ) : (
filteredDonations.map((donation) => { filteredDonations.map((donation) => {
const isExpanded = expandedRows.has(donation.id); const isExpanded = expandedRows.has(donation.id);
return ( return (
<React.Fragment key={donation.id}> <React.Fragment key={donation.id}>
<tr className="hover:bg-[var(--lavender-400)] transition-colors"> <TableRow>
<td className="px-6 py-4"> <TableCell>
<div> <div>
<p className="font-medium text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}> <p className="font-medium text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
{donation.donor_name || 'Anonymous'} {donation.donor_name || 'Anonymous'}
@@ -445,39 +433,37 @@ const AdminDonations = () => {
{donation.donor_email || 'No email'} {donation.donor_email || 'No email'}
</p> </p>
</div> </div>
</td> </TableCell>
<td className="px-6 py-4"> <TableCell>
<Badge <Badge
className={`${getTypeBadgeColor(donation.donation_type)} text-white border-none rounded-full px-3 py-1`} className={`${getTypeBadgeColor(donation.donation_type)} text-white border-none px-3 py-1`}
style={{ fontFamily: "'Inter', sans-serif" }} style={{ fontFamily: "'Inter', sans-serif" }}
> >
{donation.donation_type === 'member' ? 'Member' : 'Public'} {donation.donation_type === 'member' ? 'Member' : 'Public'}
</Badge> </Badge>
</td> </TableCell>
<td className="px-6 py-4"> <TableCell>
<p className="font-semibold text-[var(--purple-ink)] text-lg" style={{ fontFamily: "'Inter', sans-serif" }}> <p className="font-semibold text-[var(--purple-ink)] text-lg" style={{ fontFamily: "'Inter', sans-serif" }}>
{donation.amount} {donation.amount}
</p> </p>
</td> </TableCell>
<td className="px-6 py-4"> <TableCell>
<Badge variant={getStatusBadgeVariant(donation.status)} className="rounded-full"> <StatusBadge status={donation.status} />
{donation.status.charAt(0).toUpperCase() + donation.status.slice(1)} </TableCell>
</Badge> <TableCell>
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2 text-brand-purple "> <div className="flex items-center gap-2 text-brand-purple ">
<Calendar className="h-4 w-4" /> <Calendar className="h-4 w-4" />
<span style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{formatDate(donation.created_at)} {formatDate(donation.created_at)}
</span> </span>
</div> </div>
</td> </TableCell>
<td className="px-6 py-4"> <TableCell>
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif capitalize" }}>
{donation.payment_method || 'N/A'} {donation.payment_method || 'N/A'}
</p> </p>
</td> </TableCell>
<td className="px-6 py-4 text-center"> <TableCell className="text-center">
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
@@ -486,12 +472,11 @@ const AdminDonations = () => {
> >
{isExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />} {isExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</Button> </Button>
</td> </TableCell>
</tr> </TableRow>
{/* Expandable Details Row */}
{isExpanded && ( {isExpanded && (
<tr className="bg-[var(--lavender-400)]/30"> <TableRow className="bg-[var(--lavender-400)]/30">
<td colSpan="7" className="px-6 py-6"> <TableCell colSpan={7} className="p-6">
<div className="space-y-4"> <div className="space-y-4">
<h4 className="font-semibold text-[var(--purple-ink)] text-lg mb-4" style={{ fontFamily: "'Inter', sans-serif" }}> <h4 className="font-semibold text-[var(--purple-ink)] text-lg mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
Transaction Details Transaction Details
@@ -601,15 +586,15 @@ const AdminDonations = () => {
</div> </div>
</div> </div>
</div> </div>
</td> </TableCell>
</tr> </TableRow>
)} )}
</React.Fragment> </React.Fragment>
); );
}) })
)} )}
</tbody> </TableBody>
</table> </Table>
</div> </div>
</Card> </Card>

View File

@@ -18,6 +18,12 @@ import {
TableRow, TableRow,
TableCell, TableCell,
} from '../../components/ui/table'; } from '../../components/ui/table';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '../../components/ui/tooltip';
import { import {
Pagination, Pagination,
PaginationContent, PaginationContent,
@@ -417,7 +423,7 @@ const AdminValidations = () => {
<Card className="bg-background rounded-2xl border border-[var(--neutral-800)] overflow-hidden"> <Card className="bg-background rounded-2xl border border-[var(--neutral-800)] overflow-hidden">
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow className="text-md">
<TableHead <TableHead
className="cursor-pointer hover:bg-[var(--neutral-800)]/20" className="cursor-pointer hover:bg-[var(--neutral-800)]/20"
onClick={() => handleSort('first_name')} onClick={() => handleSort('first_name')}
@@ -482,8 +488,8 @@ const AdminValidations = () => {
onValueChange={(action) => handleActionSelect(user, action)} onValueChange={(action) => handleActionSelect(user, action)}
disabled={actionLoading === user.id || resendLoading === user.id} disabled={actionLoading === user.id || resendLoading === user.id}
> >
<SelectTrigger className="w-[180px] h-9 border-[var(--neutral-800)]"> <SelectTrigger className="w-[100px] h-9 border-[var(--neutral-800)]">
<SelectValue placeholder={actionLoading === user.id || resendLoading === user.id ? 'Processing...' : 'Select Action'} /> <SelectValue placeholder={actionLoading === user.id || resendLoading === user.id ? 'Processing...' : 'Action'} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{user.status === 'rejected' ? ( {user.status === 'rejected' ? (
@@ -521,29 +527,45 @@ const AdminValidations = () => {
)} )}
</SelectContent> </SelectContent>
</Select> </Select>
{/* view registration */} <TooltipProvider>
<Button {/* view registration */}
onClick={() => handleRegistrationDialog(user)} <Tooltip>
disabled={actionLoading === user.id} <TooltipTrigger asChild>
size="sm" <Button
variant="outline" onClick={() => handleRegistrationDialog(user)}
className="border-2 border-primary text-primary hover:bg-red-50 dark:hover:bg-red-500/10" disabled={actionLoading === user.id}
> size="sm"
<FileText className="size-4" /> variant="outline"
</Button> className="border-2 border-primary text-primary hover:bg-red-50 dark:hover:bg-red-500/10"
>
<FileText className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
View registration
</TooltipContent>
</Tooltip>
{/* reject */} {/* reject */}
{hasPermission('users.approve') && ( {hasPermission('users.approve') && (
<Button <Tooltip>
onClick={() => handleRejectUser(user)} <TooltipTrigger asChild>
disabled={actionLoading === user.id} <Button
size="sm" onClick={() => handleRejectUser(user)}
variant="outline" disabled={actionLoading === user.id}
className="border-2 mr-2 border-red-500 text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10" size="sm"
> variant="outline"
X className="border-2 mr-2 border-red-500 text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10"
</Button> >
)} X
</Button>
</TooltipTrigger>
<TooltipContent>
Reject user
</TooltipContent>
</Tooltip>
)}
</TooltipProvider>
</div> </div>
</TableCell> </TableCell>