Merge branch 'theme-provider' into dev

This commit is contained in:
2026-01-29 21:50:28 -06:00
9 changed files with 1464 additions and 876 deletions

View File

@@ -6,7 +6,6 @@ const STATUS_BADGE_CONFIG = {
//status-based badges
pending_email: { label: 'Pending Email', variant: 'orange2' },
pending_validation: { label: 'Pending Validation', variant: 'gray' },
pre_validated: { label: 'Pre-Validated', variant: 'green' },
payment_pending: { label: 'Payment Pending', variant: 'orange' },
active: { label: 'Active', variant: 'green' },
inactive: { label: 'Inactive', variant: 'gray2' },
@@ -23,7 +22,12 @@ const STATUS_BADGE_CONFIG = {
admin: { label: 'Admin', variant: 'purple' },
moderator: { label: 'Moderator', variant: 'bg-[var(--neutral-800)] text-[var(--purple-ink)]' },
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

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useRef, useState } from 'react';
import {
Dialog,
DialogContent,
@@ -9,12 +9,77 @@ import {
} from './ui/dialog';
import { Button } from './ui/button';
import { Card } from './ui/card';
import { Checkbox } from './ui/checkbox';
import { Input } from './ui/input';
import { Label } from './ui/label';
import { Textarea } from './ui/textarea';
import { User, Mail, Phone, Calendar, UserCheck, Clock, FileText } from 'lucide-react';
import StatusBadge from './StatusBadge';
import api from '../utils/api';
import { toast } from 'sonner';
const ViewRegistrationDialog = ({ open, onOpenChange, user }) => {
if (!user) return null;
const [formData, setFormData] = useState(null);
const [isSaving, setIsSaving] = useState(false);
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const autoSaveTimeoutRef = useRef(null);
const pendingSaveRef = useRef(false);
const leadSourceOptions = [
'Current member',
'Friend',
'OutSmart Magazine',
'Search engine (Google etc.)',
"I've known about LOAF for a long time",
'Other'
];
const volunteerOptions = [
'Welcoming new members at events',
'Sending out birthday cards',
'Care Team Calls',
'Sharing ideas for events',
'Researching grants',
'Applying for grants',
'Assisting with TeatherLOAFers',
'Assisting with ActiveLOAFers',
'Assisting with weekday Lunch Bunch',
'Uploading Photos to the Website',
'Assisting with eNewsletter',
'Other administrative task'
];
useEffect(() => {
if (!open || !user) return;
const nextFormData = {
lead_sources: Array.isArray(user.lead_sources) ? user.lead_sources : [],
partner_first_name: user.partner_first_name || '',
partner_last_name: user.partner_last_name || '',
partner_is_member: Boolean(user.partner_is_member),
partner_plan_to_become_member: Boolean(user.partner_plan_to_become_member),
newsletter_publish_name: Boolean(user.newsletter_publish_name),
newsletter_publish_photo: Boolean(user.newsletter_publish_photo),
newsletter_publish_birthday: Boolean(user.newsletter_publish_birthday),
newsletter_publish_none: Boolean(user.newsletter_publish_none),
referred_by_member_name: user.referred_by_member_name || '',
volunteer_interests: Array.isArray(user.volunteer_interests) ? user.volunteer_interests : [],
scholarship_requested: Boolean(user.scholarship_requested),
scholarship_reason: user.scholarship_reason || ''
};
setFormData(nextFormData);
setHasUnsavedChanges(false);
}, [open, user]);
useEffect(() => {
return () => {
if (autoSaveTimeoutRef.current) {
clearTimeout(autoSaveTimeoutRef.current);
}
};
}, []);
const formatDate = (dateString) => {
if (!dateString) return '—';
return new Date(dateString).toLocaleDateString('en-US', {
@@ -44,6 +109,91 @@ const ViewRegistrationDialog = ({ open, onOpenChange, user }) => {
return phone;
};
const saveProfile = async (showToast = true) => {
if (!formData) return;
if (isSaving) {
pendingSaveRef.current = true;
return;
}
setIsSaving(true);
try {
await api.put('/users/profile', {
lead_sources: formData.lead_sources,
partner_first_name: formData.partner_first_name,
partner_last_name: formData.partner_last_name,
partner_is_member: formData.partner_is_member,
partner_plan_to_become_member: formData.partner_plan_to_become_member,
newsletter_publish_name: formData.newsletter_publish_name,
newsletter_publish_photo: formData.newsletter_publish_photo,
newsletter_publish_birthday: formData.newsletter_publish_birthday,
newsletter_publish_none: formData.newsletter_publish_none,
referred_by_member_name: formData.referred_by_member_name,
volunteer_interests: formData.volunteer_interests,
scholarship_requested: formData.scholarship_requested,
scholarship_reason: formData.scholarship_reason
});
setHasUnsavedChanges(false);
if (showToast) {
toast.success('Registration details saved');
}
} catch (error) {
if (showToast) {
toast.error(error.response?.data?.detail || 'Failed to save registration details');
}
} finally {
setIsSaving(false);
if (pendingSaveRef.current) {
pendingSaveRef.current = false;
saveProfile(showToast);
}
}
};
const scheduleAutoSave = () => {
setHasUnsavedChanges(true);
if (autoSaveTimeoutRef.current) {
clearTimeout(autoSaveTimeoutRef.current);
}
autoSaveTimeoutRef.current = setTimeout(() => {
saveProfile(false);
}, 800);
};
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData(prev => {
const next = { ...prev, [name]: value };
return next;
});
scheduleAutoSave();
};
const handleCheckboxChange = (name, checked) => {
setFormData(prev => ({ ...prev, [name]: checked }));
scheduleAutoSave();
};
const handleLeadSourceChange = (source) => {
setFormData(prev => {
const sources = prev.lead_sources.includes(source)
? prev.lead_sources.filter((item) => item !== source)
: [...prev.lead_sources, source];
return { ...prev, lead_sources: sources };
});
scheduleAutoSave();
};
const handleVolunteerChange = (option) => {
setFormData(prev => {
const interests = prev.volunteer_interests.includes(option)
? prev.volunteer_interests.filter((item) => item !== option)
: [...prev.volunteer_interests, option];
return { ...prev, volunteer_interests: interests };
});
scheduleAutoSave();
};
const InfoRow = ({ icon: Icon, label, value }) => (
<div className="flex items-start gap-3 py-3 border-b border-[var(--neutral-800)] last:border-b-0">
<div className="h-10 w-10 rounded-lg bg-[var(--lavender-400)] flex items-center justify-center flex-shrink-0">
@@ -109,10 +259,214 @@ const ViewRegistrationDialog = ({ open, onOpenChange, user }) => {
Registration Details
</h3>
<InfoRow icon={Calendar} label="Registration Date" value={formatDate(user.created_at)} />
<InfoRow icon={UserCheck} label="Referred By" value={user.referred_by_member_name} />
<InfoRow icon={UserCheck} label="Referred By" value={formData?.referred_by_member_name} />
<InfoRow icon={Clock} label="Email Verification Expires" value={formatDateTime(user.email_verification_expires_at)} />
</Card>
{formData && (
<>
{/* How Did You Hear About Us */}
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
How Did You Hear About Us? *
</h3>
<div className="space-y-3">
{leadSourceOptions.map((source) => (
<div key={source} className="flex items-center space-x-2">
<Checkbox
id={`lead_${source}`}
checked={formData.lead_sources.includes(source)}
onCheckedChange={() => handleLeadSourceChange(source)}
/>
<Label htmlFor={`lead_${source}`} className="text-base cursor-pointer">
{source}
</Label>
</div>
))}
</div>
</Card>
{/* Partner Information */}
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
Partner Information (Optional)
</h3>
<div className="grid md:grid-cols-2 gap-4 mb-4">
<div>
<Label htmlFor="partner_first_name">Partner First Name</Label>
<Input
id="partner_first_name"
name="partner_first_name"
value={formData.partner_first_name}
onChange={handleInputChange}
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
/>
</div>
<div>
<Label htmlFor="partner_last_name">Partner Last Name</Label>
<Input
id="partner_last_name"
name="partner_last_name"
value={formData.partner_last_name}
onChange={handleInputChange}
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
/>
</div>
</div>
<div className="space-y-3">
<div className="flex items-center space-x-2">
<Checkbox
id="partner_is_member"
checked={formData.partner_is_member}
onCheckedChange={(checked) => handleCheckboxChange('partner_is_member', checked)}
/>
<Label htmlFor="partner_is_member" className="text-base cursor-pointer">
Is your partner already a member?
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="partner_plan_to_become_member"
checked={formData.partner_plan_to_become_member}
onCheckedChange={(checked) => handleCheckboxChange('partner_plan_to_become_member', checked)}
/>
<Label htmlFor="partner_plan_to_become_member" className="text-base cursor-pointer">
Does your partner plan to become a member?
</Label>
</div>
</div>
</Card>
{/* Newsletter Preferences */}
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
Newsletter Publication Preferences *
</h3>
<p className="text-brand-purple mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Please check what information may be published in LOAF Newsletter
</p>
<div className="space-y-3">
<div className="flex items-center space-x-2">
<Checkbox
id="newsletter_publish_name"
checked={formData.newsletter_publish_name}
onCheckedChange={(checked) => handleCheckboxChange('newsletter_publish_name', checked)}
/>
<Label htmlFor="newsletter_publish_name" className="text-base cursor-pointer">
Name
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="newsletter_publish_photo"
checked={formData.newsletter_publish_photo}
onCheckedChange={(checked) => handleCheckboxChange('newsletter_publish_photo', checked)}
/>
<Label htmlFor="newsletter_publish_photo" className="text-base cursor-pointer">
Photo (added later in profile)
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="newsletter_publish_birthday"
checked={formData.newsletter_publish_birthday}
onCheckedChange={(checked) => handleCheckboxChange('newsletter_publish_birthday', checked)}
/>
<Label htmlFor="newsletter_publish_birthday" className="text-base cursor-pointer">
Birthday
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="newsletter_publish_none"
checked={formData.newsletter_publish_none}
onCheckedChange={(checked) => handleCheckboxChange('newsletter_publish_none', checked)}
/>
<Label htmlFor="newsletter_publish_none" className="text-base cursor-pointer">
Do not publish any of my information
</Label>
</div>
</div>
</Card>
{/* Referral */}
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
Referral
</h3>
<div>
<Label htmlFor="referred_by_member_name">Name of a LOAF Member who already knows you</Label>
<Input
id="referred_by_member_name"
name="referred_by_member_name"
value={formData.referred_by_member_name}
onChange={handleInputChange}
placeholder="Enter member name or email"
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
/>
<p className="text-sm text-brand-purple mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
If referred by a current member, you may skip the event attendance requirement.
</p>
</div>
</Card>
{/* Volunteer Interests */}
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
Volunteer Interests (Optional)
</h3>
<p className="text-brand-purple mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
I may at some time be interested in volunteering with LOAF in the following ways (training is provided)
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{volunteerOptions.map((option) => (
<div key={option} className="flex items-center space-x-2">
<Checkbox
id={`volunteer_${option}`}
checked={formData.volunteer_interests.includes(option)}
onCheckedChange={() => handleVolunteerChange(option)}
/>
<Label htmlFor={`volunteer_${option}`} className="text-base cursor-pointer">
{option}
</Label>
</div>
))}
</div>
</Card>
{/* Scholarship Request */}
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
<div className="flex items-center space-x-2">
<Checkbox
id="scholarship_requested"
checked={formData.scholarship_requested}
onCheckedChange={(checked) => handleCheckboxChange('scholarship_requested', checked)}
/>
<Label htmlFor="scholarship_requested" className="text-base cursor-pointer font-semibold">
I am requesting for scholarship
</Label>
</div>
<p className="text-sm text-brand-purple mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Scholarship information is kept confidential
</p>
{formData.scholarship_requested && (
<div className="mt-4">
<Label htmlFor="scholarship_reason">Please explain your situation *</Label>
<Textarea
id="scholarship_reason"
name="scholarship_reason"
value={formData.scholarship_reason}
onChange={handleInputChange}
placeholder="Tell us why you're requesting a scholarship..."
rows={4}
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
/>
</div>
)}
</Card>
</>
)}
{/* Additional Information (if available) */}
{(user.address || user.city || user.state || user.zip_code) && (
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
@@ -156,6 +510,19 @@ const ViewRegistrationDialog = ({ open, onOpenChange, user }) => {
</div>
<DialogFooter>
<div className="flex-1 text-sm text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{isSaving && 'Saving changes...'}
{!isSaving && hasUnsavedChanges && 'Unsaved changes'}
{!isSaving && !hasUnsavedChanges && 'All changes saved'}
</div>
<Button
type="button"
onClick={() => saveProfile(true)}
disabled={!hasUnsavedChanges || isSaving}
className="rounded-xl border-2 border-[var(--neutral-800)] bg-white text-[var(--purple-ink)] hover:bg-[var(--lavender-300)]"
>
Save All
</Button>
<Button
type="button"
onClick={() => onOpenChange(false)}

View File

@@ -0,0 +1,347 @@
import React from 'react';
import { Button } from '../ui/button';
import StatusBadge from '../StatusBadge';
import {
ChevronDown,
ChevronUp,
Edit,
XCircle,
CreditCard,
Info,
ExternalLink,
Copy
} from 'lucide-react';
const HEADER_CELLS = [
{ label: 'Member', align: 'text-left' },
{ label: 'Plan', align: 'text-left' },
{ label: 'Status', align: 'text-left' },
{ label: 'Period', align: 'text-left' },
{ label: 'Base Fee', align: 'text-right' },
{ label: 'Donation', align: 'text-right' },
{ label: 'Total', align: 'text-right' },
{ label: 'Details', align: 'text-center' },
{ label: 'Actions', align: 'text-center' }
];
const HeaderCell = ({ align, children }) => (
<th
className={`p-4 text-[var(--purple-ink)] font-semibold ${align}`}
style={{ fontFamily: "'Inter', sans-serif" }}
>
{children}
</th>
);
const TableCell = ({ align = 'text-left', className = '', style, children, ...props }) => (
<td
className={`p-4 ${align} ${className}`.trim()}
style={style}
{...props}
>
{children}
</td>
);
const SubscriptionRow = ({
sub,
isExpanded,
onToggle,
onEdit,
onCancel,
hasPermission,
formatDate,
formatDateTime,
formatPrice,
copyToClipboard
}) => (
<>
<tr className="border-b border-[var(--neutral-800)] hover:bg-[var(--lavender-400)] transition-colors">
<TableCell>
<div className="font-medium text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
{sub.user.first_name} {sub.user.last_name}
</div>
<div className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{sub.user.email}
</div>
</TableCell>
<TableCell>
<div className="text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{sub.plan.name}
</div>
<div className="text-xs text-brand-purple ">
{sub.plan.billing_cycle}
</div>
</TableCell>
<TableCell>
<StatusBadge status={sub.status} />
</TableCell>
<TableCell>
<div className="text-sm text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<div>{formatDate(sub.start_date)}</div>
<div className="text-xs text-brand-purple ">to {formatDate(sub.end_date)}</div>
</div>
</TableCell>
<TableCell
align="text-right"
className="text-[var(--purple-ink)]"
style={{ fontFamily: "'Inter', sans-serif" }}
>
{formatPrice(sub.base_subscription_cents || 0)}
</TableCell>
<TableCell
align="text-right"
className="text-[var(--orange-light)]"
style={{ fontFamily: "'Inter', sans-serif" }}
>
{formatPrice(sub.donation_cents || 0)}
</TableCell>
<TableCell
align="text-right"
className="font-semibold text-[var(--purple-ink)]"
style={{ fontFamily: "'Inter', sans-serif" }}
>
{formatPrice(sub.amount_paid_cents || 0)}
</TableCell>
<TableCell>
<Button
size="sm"
variant="ghost"
onClick={onToggle}
className="text-brand-purple hover:bg-[var(--neutral-800)]"
>
{isExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</Button>
</TableCell>
<TableCell>
<div className="flex items-center justify-center gap-2">
{hasPermission('subscriptions.edit') && (
<Button
size="sm"
variant="outline"
onClick={() => onEdit(sub)}
className="text-brand-purple hover:bg-[var(--neutral-800)]"
>
<Edit className="h-4 w-4" />
</Button>
)}
{sub.status === 'active' && hasPermission('subscriptions.cancel') && (
<Button
size="sm"
variant="outline-destructive"
onClick={() => onCancel(sub.id)}
>
<XCircle className="h-4 w-4" />
</Button>
)}
</div>
</TableCell>
</tr>
{isExpanded && (
<tr className="bg-[var(--lavender-400)]/30">
<TableCell colSpan={9} className="p-6">
<div className="space-y-4">
<h4 className="font-semibold text-[var(--purple-ink)] text-lg mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
Transaction Details
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-3">
<h5 className="font-medium text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
<CreditCard className="h-4 w-4" />
Payment Information
</h5>
<div className="space-y-2 text-sm">
{sub.payment_completed_at && (
<div className="flex justify-between">
<span className="text-brand-purple ">Payment Date:</span>
<span className="text-[var(--purple-ink)] font-medium">{formatDateTime(sub.payment_completed_at)}</span>
</div>
)}
{sub.payment_method && (
<div className="flex justify-between">
<span className="text-brand-purple ">Payment Method:</span>
<span className="text-[var(--purple-ink)] font-medium capitalize">{sub.payment_method}</span>
</div>
)}
{sub.card_brand && sub.card_last4 && (
<div className="flex justify-between">
<span className="text-brand-purple ">Card:</span>
<span className="text-[var(--purple-ink)] font-medium">{sub.card_brand} ****{sub.card_last4}</span>
</div>
)}
</div>
</div>
<div className="space-y-3">
<h5 className="font-medium text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
<Info className="h-4 w-4" />
Stripe Transaction IDs
</h5>
<div className="space-y-2 text-sm">
{sub.stripe_payment_intent_id && (
<div className="flex items-center justify-between gap-2">
<span className="text-brand-purple ">Payment Intent:</span>
<div className="flex items-center gap-1">
<code className="text-xs bg-[var(--neutral-800)]/30 px-2 py-1 rounded text-[var(--purple-ink)]">
{sub.stripe_payment_intent_id.substring(0, 20)}...
</code>
<Button
size="sm"
variant="ghost"
onClick={() => copyToClipboard(sub.stripe_payment_intent_id, 'Payment Intent ID')}
>
<Copy className="h-3 w-3" />
</Button>
</div>
</div>
)}
{sub.stripe_charge_id && (
<div className="flex items-center justify-between gap-2">
<span className="text-brand-purple ">Charge ID:</span>
<div className="flex items-center gap-1">
<code className="text-xs bg-[var(--neutral-800)]/30 px-2 py-1 rounded text-[var(--purple-ink)]">
{sub.stripe_charge_id.substring(0, 20)}...
</code>
<Button
size="sm"
variant="ghost"
onClick={() => copyToClipboard(sub.stripe_charge_id, 'Charge ID')}
>
<Copy className="h-3 w-3" />
</Button>
</div>
</div>
)}
{sub.stripe_subscription_id && (
<div className="flex items-center justify-between gap-2">
<span className="text-brand-purple ">Subscription ID:</span>
<div className="flex items-center gap-1">
<code className="text-xs bg-[var(--neutral-800)]/30 px-2 py-1 rounded text-[var(--purple-ink)]">
{sub.stripe_subscription_id.substring(0, 20)}...
</code>
<Button
size="sm"
variant="ghost"
onClick={() => copyToClipboard(sub.stripe_subscription_id, 'Subscription ID')}
>
<Copy className="h-3 w-3" />
</Button>
</div>
</div>
)}
{sub.stripe_invoice_id && (
<div className="flex items-center justify-between gap-2">
<span className="text-brand-purple ">Invoice ID:</span>
<div className="flex items-center gap-1">
<code className="text-xs bg-[var(--neutral-800)]/30 px-2 py-1 rounded text-[var(--purple-ink)]">
{sub.stripe_invoice_id.substring(0, 20)}...
</code>
<Button
size="sm"
variant="ghost"
onClick={() => copyToClipboard(sub.stripe_invoice_id, 'Invoice ID')}
>
<Copy className="h-3 w-3" />
</Button>
</div>
</div>
)}
{sub.stripe_customer_id && (
<div className="flex items-center justify-between gap-2">
<span className="text-brand-purple ">Customer ID:</span>
<div className="flex items-center gap-1">
<code className="text-xs bg-[var(--neutral-800)]/30 px-2 py-1 rounded text-[var(--purple-ink)]">
{sub.stripe_customer_id.substring(0, 20)}...
</code>
<Button
size="sm"
variant="ghost"
onClick={() => copyToClipboard(sub.stripe_customer_id, 'Customer ID')}
>
<Copy className="h-3 w-3" />
</Button>
</div>
</div>
)}
{sub.stripe_receipt_url && (
<div className="flex items-center justify-between gap-2">
<span className="text-brand-purple ">Receipt:</span>
<Button
size="sm"
variant="outline"
onClick={() => window.open(sub.stripe_receipt_url, '_blank')}
className="text-brand-purple hover:bg-[var(--neutral-800)]"
>
<ExternalLink className="h-3 w-3 mr-1" />
View Receipt
</Button>
</div>
)}
</div>
</div>
</div>
</div>
</TableCell>
</tr>
)}
</>
);
const SubscriptionsTable = ({
subscriptions,
expandedRows,
onToggleRowExpansion,
onEdit,
onCancel,
hasPermission,
formatDate,
formatDateTime,
formatPrice,
copyToClipboard
}) => (
<table className="w-full">
<thead>
<tr className="bg-[var(--neutral-800)]/20 border-b border-[var(--neutral-800)]">
{HEADER_CELLS.map((cell) => (
<HeaderCell key={cell.label} align={cell.align}>
{cell.label}
</HeaderCell>
))}
</tr>
</thead>
<tbody>
{subscriptions.length > 0 ? (
subscriptions.map((sub) => (
<SubscriptionRow
key={sub.id}
sub={sub}
isExpanded={expandedRows.has(sub.id)}
onToggle={() => onToggleRowExpansion(sub.id)}
onEdit={onEdit}
onCancel={onCancel}
hasPermission={hasPermission}
formatDate={formatDate}
formatDateTime={formatDateTime}
formatPrice={formatPrice}
copyToClipboard={copyToClipboard}
/>
))
) : (
<tr>
<TableCell
align="text-center"
className="p-12 text-brand-purple "
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
colSpan={9}
>
No subscriptions found
</TableCell>
</tr>
)}
</tbody>
</table>
);
export default SubscriptionsTable;

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -323,11 +323,9 @@ const AdminMembers = () => {
<SelectItem value="active">Active</SelectItem>
<SelectItem value="payment_pending">Payment Pending</SelectItem>
<SelectItem value="pending_validation">Pending Validation</SelectItem>
<SelectItem value="pre_validated">Pre-Validated</SelectItem>
<SelectItem value="inactive">Inactive</SelectItem>
<SelectItem value="canceled">Canceled</SelectItem>
<SelectItem value="expired">Expired</SelectItem>
<SelectItem value="abandoned">Abandoned</SelectItem>
</SelectContent>
</Select>
</div>

View File

@@ -35,11 +35,14 @@ import {
FileDown,
AlertTriangle,
Info,
Repeat,
ChevronDown,
ChevronUp,
ExternalLink,
Copy,
Repeat
Copy
} from 'lucide-react';
import {
DropdownMenu,
@@ -49,6 +52,7 @@ import {
} from '../../components/ui/dropdown-menu';
import StatusBadge from '@/components/StatusBadge';
import CreateSubscriptionDialog from '@/components/CreateSubscriptionDialog';
import SubscriptionsTable from '@/components/admin/SubscriptionsTable';
const AdminSubscriptions = () => {
const { hasPermission } = useAuth();
@@ -590,277 +594,18 @@ Proceed with activation?`;
{/* Desktop Table View */}
<div className="hidden md:block overflow-x-auto">
<table className="w-full">
<thead>
<tr className="bg-[var(--neutral-800)]/20 border-b border-[var(--neutral-800)]">
<th className="text-left p-4 text-[var(--purple-ink)] font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
Member
</th>
<th className="text-left p-4 text-[var(--purple-ink)] font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
Plan
</th>
<th className="text-left p-4 text-[var(--purple-ink)] font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
Status
</th>
<th className="text-left p-4 text-[var(--purple-ink)] font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
Period
</th>
<th className="text-right p-4 text-[var(--purple-ink)] font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
Base Fee
</th>
<th className="text-right p-4 text-[var(--purple-ink)] font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
Donation
</th>
<th className="text-right p-4 text-[var(--purple-ink)] font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
Total
</th>
<th className="text-center p-4 text-[var(--purple-ink)] font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
Details
</th>
<th className="text-center p-4 text-[var(--purple-ink)] font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
Actions
</th>
</tr>
</thead>
<tbody>
{filteredSubscriptions.length > 0 ? (
filteredSubscriptions.map((sub) => {
const isExpanded = expandedRows.has(sub.id);
return (
<React.Fragment key={sub.id}>
<tr className="border-b border-[var(--neutral-800)] hover:bg-[var(--lavender-400)] transition-colors">
<td className="p-4">
<div className="font-medium text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
{sub.user.first_name} {sub.user.last_name}
</div>
<div className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{sub.user.email}
</div>
</td>
<td className="p-4">
<div className="text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{sub.plan.name}
</div>
<div className="text-xs text-brand-purple ">
{sub.plan.billing_cycle}
</div>
</td>
<td className="p-4">
<StatusBadge status={sub.status} />
</td>
<td className="p-4">
<div className="text-sm text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<div>{formatDate(sub.start_date)}</div>
<div className="text-xs text-brand-purple ">to {formatDate(sub.end_date)}</div>
</div>
</td>
<td className="p-4 text-right text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
{formatPrice(sub.base_subscription_cents || 0)}
</td>
<td className="p-4 text-right text-[var(--orange-light)]" style={{ fontFamily: "'Inter', sans-serif" }}>
{formatPrice(sub.donation_cents || 0)}
</td>
<td className="p-4 text-right font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
{formatPrice(sub.amount_paid_cents || 0)}
</td>
<td className="p-4">
<Button
size="sm"
variant="ghost"
onClick={() => toggleRowExpansion(sub.id)}
className="text-brand-purple hover:bg-[var(--neutral-800)]"
>
{isExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</Button>
</td>
<td className="p-4">
<div className="flex items-center justify-center gap-2">
{hasPermission('subscriptions.edit') && (
<Button
size="sm"
variant="outline"
onClick={() => handleEdit(sub)}
className="text-brand-purple hover:bg-[var(--neutral-800)]"
>
<Edit className="h-4 w-4" />
</Button>
)}
{sub.status === 'active' && hasPermission('subscriptions.cancel') && (
<Button
size="sm"
variant="outline-destructive"
onClick={() => handleCancelSubscription(sub.id)}
className=""
>
<XCircle className="h-4 w-4" />
</Button>
)}
</div>
</td>
</tr>
{/* Expandable Details Row */}
{isExpanded && (
<tr className="bg-[var(--lavender-400)]/30">
<td colSpan="9" className="p-6">
<div className="space-y-4">
<h4 className="font-semibold text-[var(--purple-ink)] text-lg mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
Transaction Details
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Payment Information */}
<div className="space-y-3">
<h5 className="font-medium text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
<CreditCard className="h-4 w-4" />
Payment Information
</h5>
<div className="space-y-2 text-sm">
{sub.payment_completed_at && (
<div className="flex justify-between">
<span className="text-brand-purple ">Payment Date:</span>
<span className="text-[var(--purple-ink)] font-medium">{formatDateTime(sub.payment_completed_at)}</span>
</div>
)}
{sub.payment_method && (
<div className="flex justify-between">
<span className="text-brand-purple ">Payment Method:</span>
<span className="text-[var(--purple-ink)] font-medium capitalize">{sub.payment_method}</span>
</div>
)}
{sub.card_brand && sub.card_last4 && (
<div className="flex justify-between">
<span className="text-brand-purple ">Card:</span>
<span className="text-[var(--purple-ink)] font-medium">{sub.card_brand} ****{sub.card_last4}</span>
</div>
)}
</div>
</div>
{/* Stripe Transaction IDs */}
<div className="space-y-3">
<h5 className="font-medium text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
<Info className="h-4 w-4" />
Stripe Transaction IDs
</h5>
<div className="space-y-2 text-sm">
{sub.stripe_payment_intent_id && (
<div className="flex items-center justify-between gap-2">
<span className="text-brand-purple ">Payment Intent:</span>
<div className="flex items-center gap-1">
<code className="text-xs bg-[var(--neutral-800)]/30 px-2 py-1 rounded text-[var(--purple-ink)]">
{sub.stripe_payment_intent_id.substring(0, 20)}...
</code>
<Button
size="sm"
variant="ghost"
onClick={() => copyToClipboard(sub.stripe_payment_intent_id, 'Payment Intent ID')}
>
<Copy className="h-3 w-3" />
</Button>
</div>
</div>
)}
{sub.stripe_charge_id && (
<div className="flex items-center justify-between gap-2">
<span className="text-brand-purple ">Charge ID:</span>
<div className="flex items-center gap-1">
<code className="text-xs bg-[var(--neutral-800)]/30 px-2 py-1 rounded text-[var(--purple-ink)]">
{sub.stripe_charge_id.substring(0, 20)}...
</code>
<Button
size="sm"
variant="ghost"
onClick={() => copyToClipboard(sub.stripe_charge_id, 'Charge ID')}
>
<Copy className="h-3 w-3" />
</Button>
</div>
</div>
)}
{sub.stripe_subscription_id && (
<div className="flex items-center justify-between gap-2">
<span className="text-brand-purple ">Subscription ID:</span>
<div className="flex items-center gap-1">
<code className="text-xs bg-[var(--neutral-800)]/30 px-2 py-1 rounded text-[var(--purple-ink)]">
{sub.stripe_subscription_id.substring(0, 20)}...
</code>
<Button
size="sm"
variant="ghost"
onClick={() => copyToClipboard(sub.stripe_subscription_id, 'Subscription ID')}
>
<Copy className="h-3 w-3" />
</Button>
</div>
</div>
)}
{sub.stripe_invoice_id && (
<div className="flex items-center justify-between gap-2">
<span className="text-brand-purple ">Invoice ID:</span>
<div className="flex items-center gap-1">
<code className="text-xs bg-[var(--neutral-800)]/30 px-2 py-1 rounded text-[var(--purple-ink)]">
{sub.stripe_invoice_id.substring(0, 20)}...
</code>
<Button
size="sm"
variant="ghost"
onClick={() => copyToClipboard(sub.stripe_invoice_id, 'Invoice ID')}
>
<Copy className="h-3 w-3" />
</Button>
</div>
</div>
)}
{sub.stripe_customer_id && (
<div className="flex items-center justify-between gap-2">
<span className="text-brand-purple ">Customer ID:</span>
<div className="flex items-center gap-1">
<code className="text-xs bg-[var(--neutral-800)]/30 px-2 py-1 rounded text-[var(--purple-ink)]">
{sub.stripe_customer_id.substring(0, 20)}...
</code>
<Button
size="sm"
variant="ghost"
onClick={() => copyToClipboard(sub.stripe_customer_id, 'Customer ID')}
>
<Copy className="h-3 w-3" />
</Button>
</div>
</div>
)}
{sub.stripe_receipt_url && (
<div className="flex items-center justify-between gap-2">
<span className="text-brand-purple ">Receipt:</span>
<Button
size="sm"
variant="outline"
onClick={() => window.open(sub.stripe_receipt_url, '_blank')}
className="text-brand-purple hover:bg-[var(--neutral-800)]"
>
<ExternalLink className="h-3 w-3 mr-1" />
View Receipt
</Button>
</div>
)}
</div>
</div>
</div>
</div>
</td>
</tr>
)}
</React.Fragment>
);
})
) : (
<tr>
<td colSpan="9" className="p-12 text-center text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
No subscriptions found
</td>
</tr>
)}
</tbody>
</table>
<SubscriptionsTable
subscriptions={filteredSubscriptions}
expandedRows={expandedRows}
onToggleRowExpansion={toggleRowExpansion}
onEdit={handleEdit}
onCancel={handleCancelSubscription}
hasPermission={hasPermission}
formatDate={formatDate}
formatDateTime={formatDateTime}
formatPrice={formatPrice}
copyToClipboard={copyToClipboard}
/>
</div>
</Card>

View File

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