diff --git a/src/components/TransactionHistory.js b/src/components/TransactionHistory.js
new file mode 100644
index 0000000..9843a53
--- /dev/null
+++ b/src/components/TransactionHistory.js
@@ -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 (
+
+
+
+ {isSubscription ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {transaction.description}
+
+
+ {transaction.status}
+
+
+
+
+ {formatDate(transaction.payment_completed_at || transaction.created_at)}
+ {transaction.card_brand && transaction.card_last4 && (
+ <>
+ •
+ {transaction.card_brand} ****{transaction.card_last4}
+ >
+ )}
+ {isSubscription && transaction.billing_cycle && (
+ <>
+ •
+ {transaction.billing_cycle}
+ >
+ )}
+
+ {isAdmin && transaction.manual_payment && (
+
+ Manual Payment {transaction.manual_payment_notes && `- ${transaction.manual_payment_notes}`}
+
+ )}
+
+
+
+
+
+
+ {formatAmount(transaction.amount_cents)}
+
+ {isSubscription && transaction.donation_cents > 0 && (
+
+ (incl. {formatAmount(transaction.donation_cents)} donation)
+
+ )}
+
+ {transaction.stripe_receipt_url && (
+
+
+
+ )}
+
+
+ );
+ };
+
+ const EmptyState = ({ type }) => (
+
+
+ {type === 'subscription' ? (
+
+ ) : type === 'donation' ? (
+
+ ) : (
+
+ )}
+
+
+ {type === 'subscription'
+ ? 'No subscription payments yet'
+ : type === 'donation'
+ ? 'No donations yet'
+ : 'No transactions yet'}
+
+
+ );
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ Transaction History
+
+
+
+ {/* Summary Cards */}
+
+
+
+
+ Total Subscriptions
+
+
+ {formatAmount(totalSubscriptionCents)}
+
+
{subscriptions.length} payment(s)
+
+
+
+
+ Total Donations
+
+
+ {formatAmount(totalDonationCents)}
+
+
{donations.length} donation(s)
+
+
+
+ {/* Tabs */}
+
+
+
+ All ({allTransactions.length})
+
+
+ Subscriptions ({subscriptions.length})
+
+
+ Donations ({donations.length})
+
+
+
+
+
+ {allTransactions.length > 0 ? (
+ allTransactions.map((transaction) => (
+
+ ))
+ ) : (
+
+ )}
+
+
+
+ {subscriptions.length > 0 ? (
+ subscriptions.map((transaction) => (
+
+ ))
+ ) : (
+
+ )}
+
+
+
+ {donations.length > 0 ? (
+ donations.map((transaction) => (
+
+ ))
+ ) : (
+
+ )}
+
+
+
+
+ );
+};
+
+export default TransactionHistory;
diff --git a/src/pages/Profile.js b/src/pages/Profile.js
index 4c51fbf..d270dfa 100644
--- a/src/pages/Profile.js
+++ b/src/pages/Profile.js
@@ -12,6 +12,7 @@ import MemberFooter from '../components/MemberFooter';
import { User, Save, Lock, Heart, Users, Mail, BookUser, Camera, Upload, Trash2 } from 'lucide-react';
import { Avatar, AvatarImage, AvatarFallback } from '../components/ui/avatar';
import ChangePasswordDialog from '../components/ChangePasswordDialog';
+import TransactionHistory from '../components/TransactionHistory';
const Profile = () => {
const { user } = useAuth();
@@ -24,6 +25,8 @@ const Profile = () => {
const fileInputRef = useRef(null);
const [maxFileSizeMB, setMaxFileSizeMB] = useState(50); // Default 50MB
const [maxFileSizeBytes, setMaxFileSizeBytes] = useState(52428800); // Default 50MB in bytes
+ const [transactions, setTransactions] = useState({ subscriptions: [], donations: [] });
+ const [transactionsLoading, setTransactionsLoading] = useState(true);
const [formData, setFormData] = useState({
// Personal Information
first_name: '',
@@ -58,6 +61,7 @@ const Profile = () => {
useEffect(() => {
fetchConfig();
fetchProfile();
+ fetchTransactions();
}, []);
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 { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
@@ -703,6 +720,18 @@ const Profile = () => {
open={passwordDialogOpen}
onOpenChange={setPasswordDialogOpen}
/>
+
+ {/* Transaction History Section */}
+
+
+
diff --git a/src/pages/admin/AdminUserView.js b/src/pages/admin/AdminUserView.js
index 350880a..e2cdb77 100644
--- a/src/pages/admin/AdminUserView.js
+++ b/src/pages/admin/AdminUserView.js
@@ -10,6 +10,7 @@ import { ArrowLeft, Mail, Phone, MapPin, Calendar, Lock, AlertTriangle, Camera,
import { toast } from 'sonner';
import ConfirmationDialog from '../../components/ConfirmationDialog';
import ChangeRoleDialog from '../../components/ChangeRoleDialog';
+import TransactionHistory from '../../components/TransactionHistory';
const AdminUserView = () => {
const { userId } = useParams();
@@ -18,8 +19,8 @@ const AdminUserView = () => {
const [loading, setLoading] = useState(true);
const [resetPasswordLoading, setResetPasswordLoading] = useState(false);
const [resendVerificationLoading, setResendVerificationLoading] = useState(false);
- const [subscriptions, setSubscriptions] = useState([]);
- const [subscriptionsLoading, setSubscriptionsLoading] = useState(true);
+ const [transactions, setTransactions] = useState({ subscriptions: [], donations: [] });
+ const [transactionsLoading, setTransactionsLoading] = useState(true);
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
const [pendingAction, setPendingAction] = useState(null);
const [uploadingPhoto, setUploadingPhoto] = useState(false);
@@ -68,7 +69,7 @@ const AdminUserView = () => {
useEffect(() => {
fetchConfig();
fetchUserProfile();
- fetchSubscriptions();
+ fetchTransactions();
}, [userId]);
useEffect(() => {
@@ -89,14 +90,15 @@ const AdminUserView = () => {
}
};
- const fetchSubscriptions = async () => {
+ const fetchTransactions = async () => {
try {
- const response = await api.get(`/admin/subscriptions?user_id=${userId}`);
- setSubscriptions(response.data);
+ setTransactionsLoading(true);
+ const response = await api.get(`/admin/users/${userId}/transactions`);
+ setTransactions(response.data);
} catch (error) {
- console.error('Failed to fetch subscriptions:', error);
+ console.error('Failed to fetch transactions:', error);
} finally {
- setSubscriptionsLoading(false);
+ setTransactionsLoading(false);
}
};
@@ -482,97 +484,17 @@ const AdminUserView = () => {
- {/* Subscription Info (if applicable) */}
- {user.role === 'member' && (
-
-
- Subscription Information
-
-
- {subscriptionsLoading ? (
- Loading subscriptions...
- ) : subscriptions.length === 0 ? (
- No subscriptions found for this member.
- ) : (
-
- {subscriptions.map((sub) => (
-
-
-
-
- {sub.plan.name}
-
-
- {sub.plan.billing_cycle}
-
-
-
- {sub.status}
-
-
-
-
-
-
-
- {formatDateDisplayValue(sub.start_date)}
-
-
- {sub.end_date && (
-
-
-
- {formatDateDisplayValue(sub.end_date)}
-
-
- )}
-
-
-
- ${(sub.base_subscription_cents / 100).toFixed(2)}
-
-
- {sub.donation_cents > 0 && (
-
-
-
- ${(sub.donation_cents / 100).toFixed(2)}
-
-
- )}
-
-
-
- ${(sub.amount_paid_cents / 100).toFixed(2)}
-
-
- {sub.payment_method && (
-
-
-
- {sub.payment_method}
-
-
- )}
- {sub.stripe_subscription_id && (
-
-
-
- {sub.stripe_subscription_id}
-
-
- )}
-
-
- ))}
-
- )}
-
- )}
+ {/* Transaction History */}
+
+
+
{/* Admin Action Confirmation Dialog */}