Compare commits
10 Commits
91e264bf7a
...
dav-prod
| Author | SHA1 | Date | |
|---|---|---|---|
| 48c5a916d9 | |||
|
|
002ef5c897 | ||
| 7d0c207f1b | |||
| 1f9e6ea191 | |||
| 66c2bedbed | |||
| d94ea7b6d5 | |||
| 24519a7080 | |||
| b1b9a05d4f | |||
| a2070b4e4e | |||
| 6a21d32319 |
252
src/components/TransactionHistory.js
Normal file
252
src/components/TransactionHistory.js
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Card } from './ui/card';
|
||||||
|
import { Badge } from './ui/badge';
|
||||||
|
import { Button } from './ui/button';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
|
||||||
|
import { Receipt, CreditCard, Heart, Calendar, ExternalLink, DollarSign } from 'lucide-react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TransactionHistory Component
|
||||||
|
* Displays user transaction history including subscriptions and donations
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {Array} props.subscriptions - List of subscription transactions
|
||||||
|
* @param {Array} props.donations - List of donation transactions
|
||||||
|
* @param {number} props.totalSubscriptionCents - Total subscription amount in cents
|
||||||
|
* @param {number} props.totalDonationCents - Total donation amount in cents
|
||||||
|
* @param {boolean} props.loading - Loading state
|
||||||
|
* @param {boolean} props.isAdmin - Whether viewing as admin (shows extra fields)
|
||||||
|
*/
|
||||||
|
const TransactionHistory = ({
|
||||||
|
subscriptions = [],
|
||||||
|
donations = [],
|
||||||
|
totalSubscriptionCents = 0,
|
||||||
|
totalDonationCents = 0,
|
||||||
|
loading = false,
|
||||||
|
isAdmin = false
|
||||||
|
}) => {
|
||||||
|
const [activeTab, setActiveTab] = useState('all');
|
||||||
|
|
||||||
|
const formatAmount = (cents) => {
|
||||||
|
if (!cents) return '$0.00';
|
||||||
|
return `$${(cents / 100).toFixed(2)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
if (!dateString) return 'N/A';
|
||||||
|
return new Date(dateString).toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusBadgeClass = (status) => {
|
||||||
|
switch (status?.toLowerCase()) {
|
||||||
|
case 'active':
|
||||||
|
case 'completed':
|
||||||
|
return 'bg-green-100 text-green-800 border-green-200';
|
||||||
|
case 'pending':
|
||||||
|
return 'bg-yellow-100 text-yellow-800 border-yellow-200';
|
||||||
|
case 'cancelled':
|
||||||
|
case 'failed':
|
||||||
|
case 'expired':
|
||||||
|
return 'bg-red-100 text-red-800 border-red-200';
|
||||||
|
default:
|
||||||
|
return 'bg-gray-100 text-gray-800 border-gray-200';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const allTransactions = [
|
||||||
|
...subscriptions.map(s => ({ ...s, sortDate: s.created_at })),
|
||||||
|
...donations.map(d => ({ ...d, sortDate: d.created_at }))
|
||||||
|
].sort((a, b) => new Date(b.sortDate) - new Date(a.sortDate));
|
||||||
|
|
||||||
|
const TransactionRow = ({ transaction }) => {
|
||||||
|
const isSubscription = transaction.type === 'subscription';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between p-4 border-b border-[var(--neutral-800)] last:border-b-0 hover:bg-[var(--lavender-500)] transition-colors">
|
||||||
|
<div className="flex items-start gap-3 mb-2 sm:mb-0">
|
||||||
|
<div className={`p-2 rounded-lg ${isSubscription ? 'bg-[var(--purple-lavender)] bg-opacity-20' : 'bg-[var(--orange-light)] bg-opacity-20'}`}>
|
||||||
|
{isSubscription ? (
|
||||||
|
<CreditCard className="h-5 w-5 text-[var(--purple-lavender)]" />
|
||||||
|
) : (
|
||||||
|
<Heart className="h-5 w-5 text-[var(--orange-light)]" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<span className="font-medium text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
{transaction.description}
|
||||||
|
</span>
|
||||||
|
<Badge className={`text-xs ${getStatusBadgeClass(transaction.status)}`}>
|
||||||
|
{transaction.status}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-sm text-brand-purple mt-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
<Calendar className="h-3 w-3" />
|
||||||
|
<span>{formatDate(transaction.payment_completed_at || transaction.created_at)}</span>
|
||||||
|
{transaction.card_brand && transaction.card_last4 && (
|
||||||
|
<>
|
||||||
|
<span className="text-[var(--neutral-800)]">•</span>
|
||||||
|
<span>{transaction.card_brand} ****{transaction.card_last4}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isSubscription && transaction.billing_cycle && (
|
||||||
|
<>
|
||||||
|
<span className="text-[var(--neutral-800)]">•</span>
|
||||||
|
<span className="capitalize">{transaction.billing_cycle}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{isAdmin && transaction.manual_payment && (
|
||||||
|
<div className="text-xs text-[var(--orange-light)] mt-1">
|
||||||
|
Manual Payment {transaction.manual_payment_notes && `- ${transaction.manual_payment_notes}`}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 pl-10 sm:pl-0">
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
{formatAmount(transaction.amount_cents)}
|
||||||
|
</div>
|
||||||
|
{isSubscription && transaction.donation_cents > 0 && (
|
||||||
|
<div className="text-xs text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
(incl. {formatAmount(transaction.donation_cents)} donation)
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{transaction.stripe_receipt_url && (
|
||||||
|
<a
|
||||||
|
href={transaction.stripe_receipt_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="p-2 text-brand-purple hover:text-[var(--purple-ink)] hover:bg-[var(--lavender-300)] rounded-lg transition-colors"
|
||||||
|
title="View Receipt"
|
||||||
|
>
|
||||||
|
<ExternalLink className="h-4 w-4" />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const EmptyState = ({ type }) => (
|
||||||
|
<div className="py-12 text-center">
|
||||||
|
<div className="mx-auto w-16 h-16 bg-[var(--lavender-300)] rounded-full flex items-center justify-center mb-4">
|
||||||
|
{type === 'subscription' ? (
|
||||||
|
<CreditCard className="h-8 w-8 text-brand-purple" />
|
||||||
|
) : type === 'donation' ? (
|
||||||
|
<Heart className="h-8 w-8 text-brand-purple" />
|
||||||
|
) : (
|
||||||
|
<Receipt className="h-8 w-8 text-brand-purple" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
{type === 'subscription'
|
||||||
|
? 'No subscription payments yet'
|
||||||
|
: type === 'donation'
|
||||||
|
? 'No donations yet'
|
||||||
|
: 'No transactions yet'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Card className="p-8 bg-background rounded-2xl border border-[var(--neutral-800)]">
|
||||||
|
<div className="flex items-center justify-center py-12">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[var(--purple-lavender)]"></div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)]">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h2 className="text-2xl font-semibold text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
<Receipt className="h-6 w-6 text-brand-purple" />
|
||||||
|
Transaction History
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Summary Cards */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-6">
|
||||||
|
<div className="p-4 bg-[var(--lavender-500)] rounded-xl border border-[var(--neutral-800)]">
|
||||||
|
<div className="flex items-center gap-2 text-brand-purple mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
<CreditCard className="h-4 w-4" />
|
||||||
|
<span className="text-sm">Total Subscriptions</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
{formatAmount(totalSubscriptionCents)}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-brand-purple mt-1">{subscriptions.length} payment(s)</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 bg-[var(--lavender-500)] rounded-xl border border-[var(--neutral-800)]">
|
||||||
|
<div className="flex items-center gap-2 text-brand-purple mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
<Heart className="h-4 w-4" />
|
||||||
|
<span className="text-sm">Total Donations</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
{formatAmount(totalDonationCents)}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-brand-purple mt-1">{donations.length} donation(s)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||||
|
<TabsList className="grid w-full grid-cols-3 mb-4">
|
||||||
|
<TabsTrigger value="all" className="data-[state=active]:bg-[var(--purple-lavender)] data-[state=active]:text-white">
|
||||||
|
All ({allTransactions.length})
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="subscriptions" className="data-[state=active]:bg-[var(--purple-lavender)] data-[state=active]:text-white">
|
||||||
|
Subscriptions ({subscriptions.length})
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="donations" className="data-[state=active]:bg-[var(--purple-lavender)] data-[state=active]:text-white">
|
||||||
|
Donations ({donations.length})
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<div className="border border-[var(--neutral-800)] rounded-xl overflow-hidden">
|
||||||
|
<TabsContent value="all" className="m-0">
|
||||||
|
{allTransactions.length > 0 ? (
|
||||||
|
allTransactions.map((transaction) => (
|
||||||
|
<TransactionRow key={transaction.id} transaction={transaction} />
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<EmptyState type="all" />
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="subscriptions" className="m-0">
|
||||||
|
{subscriptions.length > 0 ? (
|
||||||
|
subscriptions.map((transaction) => (
|
||||||
|
<TransactionRow key={transaction.id} transaction={transaction} />
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<EmptyState type="subscription" />
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="donations" className="m-0">
|
||||||
|
{donations.length > 0 ? (
|
||||||
|
donations.map((transaction) => (
|
||||||
|
<TransactionRow key={transaction.id} transaction={transaction} />
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<EmptyState type="donation" />
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
</div>
|
||||||
|
</Tabs>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TransactionHistory;
|
||||||
@@ -12,6 +12,7 @@ import MemberFooter from '../components/MemberFooter';
|
|||||||
import { User, Save, Lock, Heart, Users, Mail, BookUser, Camera, Upload, Trash2 } from 'lucide-react';
|
import { User, Save, Lock, Heart, Users, Mail, BookUser, Camera, Upload, Trash2 } from 'lucide-react';
|
||||||
import { Avatar, AvatarImage, AvatarFallback } from '../components/ui/avatar';
|
import { Avatar, AvatarImage, AvatarFallback } from '../components/ui/avatar';
|
||||||
import ChangePasswordDialog from '../components/ChangePasswordDialog';
|
import ChangePasswordDialog from '../components/ChangePasswordDialog';
|
||||||
|
import TransactionHistory from '../components/TransactionHistory';
|
||||||
|
|
||||||
const Profile = () => {
|
const Profile = () => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
@@ -24,6 +25,8 @@ const Profile = () => {
|
|||||||
const fileInputRef = useRef(null);
|
const fileInputRef = useRef(null);
|
||||||
const [maxFileSizeMB, setMaxFileSizeMB] = useState(50); // Default 50MB
|
const [maxFileSizeMB, setMaxFileSizeMB] = useState(50); // Default 50MB
|
||||||
const [maxFileSizeBytes, setMaxFileSizeBytes] = useState(52428800); // Default 50MB in bytes
|
const [maxFileSizeBytes, setMaxFileSizeBytes] = useState(52428800); // Default 50MB in bytes
|
||||||
|
const [transactions, setTransactions] = useState({ subscriptions: [], donations: [] });
|
||||||
|
const [transactionsLoading, setTransactionsLoading] = useState(true);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
// Personal Information
|
// Personal Information
|
||||||
first_name: '',
|
first_name: '',
|
||||||
@@ -58,6 +61,7 @@ const Profile = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchConfig();
|
fetchConfig();
|
||||||
fetchProfile();
|
fetchProfile();
|
||||||
|
fetchTransactions();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchConfig = async () => {
|
const fetchConfig = async () => {
|
||||||
@@ -112,6 +116,19 @@ const Profile = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchTransactions = async () => {
|
||||||
|
try {
|
||||||
|
setTransactionsLoading(true);
|
||||||
|
const response = await api.get('/members/transactions');
|
||||||
|
setTransactions(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load transactions:', error);
|
||||||
|
// Don't show error toast - transactions are optional
|
||||||
|
} finally {
|
||||||
|
setTransactionsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleInputChange = (e) => {
|
const handleInputChange = (e) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
setFormData(prev => ({ ...prev, [name]: value }));
|
setFormData(prev => ({ ...prev, [name]: value }));
|
||||||
@@ -703,6 +720,18 @@ const Profile = () => {
|
|||||||
open={passwordDialogOpen}
|
open={passwordDialogOpen}
|
||||||
onOpenChange={setPasswordDialogOpen}
|
onOpenChange={setPasswordDialogOpen}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Transaction History Section */}
|
||||||
|
<div className="mt-8">
|
||||||
|
<TransactionHistory
|
||||||
|
subscriptions={transactions.subscriptions}
|
||||||
|
donations={transactions.donations}
|
||||||
|
totalSubscriptionCents={transactions.total_subscription_amount_cents}
|
||||||
|
totalDonationCents={transactions.total_donation_amount_cents}
|
||||||
|
loading={transactionsLoading}
|
||||||
|
isAdmin={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MemberFooter />
|
<MemberFooter />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { ArrowLeft, Mail, Phone, MapPin, Calendar, Lock, AlertTriangle, Camera,
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import ConfirmationDialog from '../../components/ConfirmationDialog';
|
import ConfirmationDialog from '../../components/ConfirmationDialog';
|
||||||
import ChangeRoleDialog from '../../components/ChangeRoleDialog';
|
import ChangeRoleDialog from '../../components/ChangeRoleDialog';
|
||||||
|
import TransactionHistory from '../../components/TransactionHistory';
|
||||||
|
|
||||||
const AdminUserView = () => {
|
const AdminUserView = () => {
|
||||||
const { userId } = useParams();
|
const { userId } = useParams();
|
||||||
@@ -18,8 +19,8 @@ const AdminUserView = () => {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [resetPasswordLoading, setResetPasswordLoading] = useState(false);
|
const [resetPasswordLoading, setResetPasswordLoading] = useState(false);
|
||||||
const [resendVerificationLoading, setResendVerificationLoading] = useState(false);
|
const [resendVerificationLoading, setResendVerificationLoading] = useState(false);
|
||||||
const [subscriptions, setSubscriptions] = useState([]);
|
const [transactions, setTransactions] = useState({ subscriptions: [], donations: [] });
|
||||||
const [subscriptionsLoading, setSubscriptionsLoading] = useState(true);
|
const [transactionsLoading, setTransactionsLoading] = useState(true);
|
||||||
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
|
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
|
||||||
const [pendingAction, setPendingAction] = useState(null);
|
const [pendingAction, setPendingAction] = useState(null);
|
||||||
const [uploadingPhoto, setUploadingPhoto] = useState(false);
|
const [uploadingPhoto, setUploadingPhoto] = useState(false);
|
||||||
@@ -68,7 +69,7 @@ const AdminUserView = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchConfig();
|
fetchConfig();
|
||||||
fetchUserProfile();
|
fetchUserProfile();
|
||||||
fetchSubscriptions();
|
fetchTransactions();
|
||||||
}, [userId]);
|
}, [userId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -89,14 +90,15 @@ const AdminUserView = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchSubscriptions = async () => {
|
const fetchTransactions = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await api.get(`/admin/subscriptions?user_id=${userId}`);
|
setTransactionsLoading(true);
|
||||||
setSubscriptions(response.data);
|
const response = await api.get(`/admin/users/${userId}/transactions`);
|
||||||
|
setTransactions(response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch subscriptions:', error);
|
console.error('Failed to fetch transactions:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setSubscriptionsLoading(false);
|
setTransactionsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -482,97 +484,17 @@ const AdminUserView = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Subscription Info (if applicable) */}
|
{/* Transaction History */}
|
||||||
{user.role === 'member' && (
|
<div className="mt-8">
|
||||||
<Card className="p-8 bg-background rounded-2xl border border-[var(--neutral-800)] mt-8">
|
<TransactionHistory
|
||||||
<h2 className="text-2xl font-semibold text-[var(--purple-ink)] mb-6" style={{ fontFamily: "'Inter', sans-serif" }}>
|
subscriptions={transactions.subscriptions}
|
||||||
Subscription Information
|
donations={transactions.donations}
|
||||||
</h2>
|
totalSubscriptionCents={transactions.total_subscription_amount_cents}
|
||||||
|
totalDonationCents={transactions.total_donation_amount_cents}
|
||||||
{subscriptionsLoading ? (
|
loading={transactionsLoading}
|
||||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading subscriptions...</p>
|
isAdmin={true}
|
||||||
) : subscriptions.length === 0 ? (
|
/>
|
||||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>No subscriptions found for this member.</p>
|
</div>
|
||||||
) : (
|
|
||||||
<div className="space-y-6">
|
|
||||||
{subscriptions.map((sub) => (
|
|
||||||
<div key={sub.id} className="p-6 bg-[var(--lavender-500)] rounded-xl border border-[var(--neutral-800)]">
|
|
||||||
<div className="flex items-start justify-between mb-4">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
{sub.plan.name}
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
{sub.plan.billing_cycle}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Badge className={
|
|
||||||
sub.status === 'active' ? 'bg-[var(--green-light)] text-white' :
|
|
||||||
sub.status === 'expired' ? 'bg-red-500 text-white' :
|
|
||||||
'bg-gray-400 text-white'
|
|
||||||
}>
|
|
||||||
{sub.status}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 gap-4 text-sm">
|
|
||||||
<div>
|
|
||||||
<label className="text-brand-purple font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Start Date</label>
|
|
||||||
<p className="text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
{formatDateDisplayValue(sub.start_date)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{sub.end_date && (
|
|
||||||
<div>
|
|
||||||
<label className="text-brand-purple font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>End Date</label>
|
|
||||||
<p className="text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
{formatDateDisplayValue(sub.end_date)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div>
|
|
||||||
<label className="text-brand-purple font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Base Amount</label>
|
|
||||||
<p className="text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
${(sub.base_subscription_cents / 100).toFixed(2)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{sub.donation_cents > 0 && (
|
|
||||||
<div>
|
|
||||||
<label className="text-brand-purple font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Donation</label>
|
|
||||||
<p className="text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
${(sub.donation_cents / 100).toFixed(2)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div>
|
|
||||||
<label className="text-brand-purple font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Total Paid</label>
|
|
||||||
<p className="text-[var(--purple-ink)] font-semibold" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
${(sub.amount_paid_cents / 100).toFixed(2)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{sub.payment_method && (
|
|
||||||
<div>
|
|
||||||
<label className="text-brand-purple font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Payment Method</label>
|
|
||||||
<p className="text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
{sub.payment_method}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{sub.stripe_subscription_id && (
|
|
||||||
<div className="md:col-span-2">
|
|
||||||
<label className="text-brand-purple font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Stripe Subscription ID</label>
|
|
||||||
<p className="text-[var(--purple-ink)] text-xs font-mono" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
{sub.stripe_subscription_id}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Admin Action Confirmation Dialog */}
|
{/* Admin Action Confirmation Dialog */}
|
||||||
<ConfirmationDialog
|
<ConfirmationDialog
|
||||||
|
|||||||
Reference in New Issue
Block a user