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 */}