From c79db66739022f3d8c618800f069df87ab46db9d Mon Sep 17 00:00:00 2001
From: Koncept Kit <63216427+konceptkit@users.noreply.github.com>
Date: Tue, 20 Jan 2026 23:52:35 +0700
Subject: [PATCH] - Details Column - Expandable chevron button for each row-
Expandable Transaction Details - Click chevron to show/hide details- Payment
Information Section:- Stripe Transaction IDs Section- Copy to Clipboard -
One-click copy for all transaction IDs- Update Stripe webhook event
permission on Stripe Config page.
---
src/pages/admin/AdminDonations.js | 256 ++++++++++++++++----
src/pages/admin/AdminSettings.js | 36 ++-
src/pages/admin/AdminSubscriptions.js | 327 +++++++++++++++++++++-----
3 files changed, 503 insertions(+), 116 deletions(-)
diff --git a/src/pages/admin/AdminDonations.js b/src/pages/admin/AdminDonations.js
index 248ecc5..4e53094 100644
--- a/src/pages/admin/AdminDonations.js
+++ b/src/pages/admin/AdminDonations.js
@@ -28,7 +28,13 @@ import {
Loader2,
Download,
FileDown,
- Calendar
+ Calendar,
+ ChevronDown,
+ ChevronUp,
+ ExternalLink,
+ Copy,
+ CreditCard,
+ Info
} from 'lucide-react';
const AdminDonations = () => {
@@ -43,6 +49,7 @@ const AdminDonations = () => {
const [statusFilter, setStatusFilter] = useState('all');
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
+ const [expandedRows, setExpandedRows] = useState(new Set());
useEffect(() => {
fetchData();
@@ -157,6 +164,27 @@ const AdminDonations = () => {
});
};
+ const toggleRowExpansion = (donationId) => {
+ setExpandedRows((prev) => {
+ const newExpanded = new Set(prev);
+ if (newExpanded.has(donationId)) {
+ newExpanded.delete(donationId);
+ } else {
+ newExpanded.add(donationId);
+ }
+ return newExpanded;
+ });
+ };
+
+ const copyToClipboard = async (text, label) => {
+ try {
+ await navigator.clipboard.writeText(text);
+ toast.success(`${label} copied to clipboard`);
+ } catch (error) {
+ toast.error('Failed to copy to clipboard');
+ }
+ };
+
const getStatusBadgeVariant = (status) => {
const variants = {
completed: 'default',
@@ -385,12 +413,15 @@ const AdminDonations = () => {
Payment Method
|
+
+ Details
+ |
{filteredDonations.length === 0 ? (
- |
+ |
@@ -400,51 +431,182 @@ const AdminDonations = () => {
|
) : (
- filteredDonations.map((donation) => (
-
-
-
-
- {donation.donor_name || 'Anonymous'}
-
-
- {donation.donor_email || 'No email'}
-
-
- |
-
-
- {donation.donation_type === 'member' ? 'Member' : 'Public'}
-
- |
-
-
- {donation.amount}
-
- |
-
-
- {donation.status.charAt(0).toUpperCase() + donation.status.slice(1)}
-
- |
-
-
-
-
- {formatDate(donation.created_at)}
-
-
- |
-
-
- {donation.payment_method || 'N/A'}
-
- |
-
- ))
+ filteredDonations.map((donation) => {
+ const isExpanded = expandedRows.has(donation.id);
+ return (
+
+
+
+
+
+ {donation.donor_name || 'Anonymous'}
+
+
+ {donation.donor_email || 'No email'}
+
+
+ |
+
+
+ {donation.donation_type === 'member' ? 'Member' : 'Public'}
+
+ |
+
+
+ {donation.amount}
+
+ |
+
+
+ {donation.status.charAt(0).toUpperCase() + donation.status.slice(1)}
+
+ |
+
+
+
+
+ {formatDate(donation.created_at)}
+
+
+ |
+
+
+ {donation.payment_method || 'N/A'}
+
+ |
+
+
+ |
+
+ {/* Expandable Details Row */}
+ {isExpanded && (
+
+
+
+
+ Transaction Details
+
+
+ {/* Payment Information */}
+
+
+
+ Payment Information
+
+
+ {donation.payment_completed_at && (
+
+ Payment Date:
+ {formatDate(donation.payment_completed_at)}
+
+ )}
+ {donation.payment_method && (
+
+ Payment Method:
+ {donation.payment_method}
+
+ )}
+ {donation.card_brand && donation.card_last4 && (
+
+ Card:
+ {donation.card_brand} ****{donation.card_last4}
+
+ )}
+
+
+
+ {/* Stripe Transaction IDs */}
+
+
+
+ Stripe Transaction IDs
+
+
+ {donation.stripe_payment_intent_id && (
+
+ Payment Intent:
+
+
+ {donation.stripe_payment_intent_id.substring(0, 20)}...
+
+
+
+
+ )}
+ {donation.stripe_charge_id && (
+
+ Charge ID:
+
+
+ {donation.stripe_charge_id.substring(0, 20)}...
+
+
+
+
+ )}
+ {donation.stripe_customer_id && (
+
+ Customer ID:
+
+
+ {donation.stripe_customer_id.substring(0, 20)}...
+
+
+
+
+ )}
+ {donation.stripe_receipt_url && (
+
+ Receipt:
+
+
+ )}
+
+
+
+
+ |
+
+ )}
+
+ );
+ })
)}
diff --git a/src/pages/admin/AdminSettings.js b/src/pages/admin/AdminSettings.js
index 6967086..5d106c6 100644
--- a/src/pages/admin/AdminSettings.js
+++ b/src/pages/admin/AdminSettings.js
@@ -342,16 +342,26 @@ export default function AdminSettings() {
-
Webhook Events:
+
Webhook Events to Configure:
-
✅ Currently Handled:
+
✅ Required (Configure in Stripe):
- - checkout.session.completed - Subscription & donation payments
+ - checkout.session.completed - Handles subscriptions & donations
+
+
🔔 Automatically Triggered:
+
+ - payment_intent.created
+ - payment_intent.succeeded
+ - charge.succeeded
+ - charge.updated
+
+
These fire automatically with checkout.session.completed
+
-
🔄 Coming Soon:
+
🔄 Coming Soon (Recurring Subscriptions):
- invoice.payment_succeeded
- invoice.payment_failed
@@ -417,16 +427,26 @@ export default function AdminSettings() {
-
Webhook Events:
+
Webhook Events to Configure:
-
✅ Currently Handled:
+
✅ Required (Configure in Stripe):
- - checkout.session.completed - Subscription & donation payments
+ - checkout.session.completed - Handles subscriptions & donations
+
+
🔔 Automatically Triggered:
+
+ - payment_intent.created
+ - payment_intent.succeeded
+ - charge.succeeded
+ - charge.updated
+
+
These fire automatically with checkout.session.completed
+
-
🔄 Coming Soon:
+
🔄 Coming Soon (Recurring Subscriptions):
- invoice.payment_succeeded
- invoice.payment_failed
diff --git a/src/pages/admin/AdminSubscriptions.js b/src/pages/admin/AdminSubscriptions.js
index bb0e4c5..815580d 100644
--- a/src/pages/admin/AdminSubscriptions.js
+++ b/src/pages/admin/AdminSubscriptions.js
@@ -35,7 +35,11 @@ import {
Download,
FileDown,
AlertTriangle,
- Info
+ Info,
+ ChevronDown,
+ ChevronUp,
+ ExternalLink,
+ Copy
} from 'lucide-react';
import {
DropdownMenu,
@@ -55,6 +59,7 @@ const AdminSubscriptions = () => {
const [statusFilter, setStatusFilter] = useState('all');
const [planFilter, setPlanFilter] = useState('all');
const [exporting, setExporting] = useState(false);
+ const [expandedRows, setExpandedRows] = useState(new Set());
// Edit subscription dialog state
const [editDialogOpen, setEditDialogOpen] = useState(false);
@@ -265,6 +270,38 @@ Proceed with activation?`;
});
};
+ const formatDateTime = (dateString) => {
+ if (!dateString) return 'N/A';
+ return new Date(dateString).toLocaleString('en-US', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+ };
+
+ const toggleRowExpansion = (subscriptionId) => {
+ setExpandedRows((prev) => {
+ const newExpanded = new Set(prev);
+ if (newExpanded.has(subscriptionId)) {
+ newExpanded.delete(subscriptionId);
+ } else {
+ newExpanded.add(subscriptionId);
+ }
+ return newExpanded;
+ });
+ };
+
+ const copyToClipboard = async (text, label) => {
+ try {
+ await navigator.clipboard.writeText(text);
+ toast.success(`${label} copied to clipboard`);
+ } catch (error) {
+ toast.error('Failed to copy to clipboard');
+ }
+ };
+
const getStatusBadgeVariant = (status) => {
const variants = {
active: 'default',
@@ -566,6 +603,9 @@ Proceed with activation?`;
Total
|
+
+ Details
+ |
Actions
|
@@ -573,73 +613,238 @@ Proceed with activation?`;
{filteredSubscriptions.length > 0 ? (
- filteredSubscriptions.map((sub) => (
-
- |
-
- {sub.user.first_name} {sub.user.last_name}
-
-
- {sub.user.email}
-
- |
-
-
- {sub.plan.name}
-
-
- {sub.plan.billing_cycle}
-
- |
-
-
- {sub.status}
-
- |
-
-
- {formatDate(sub.start_date)}
- to {formatDate(sub.end_date)}
-
- |
-
- {formatPrice(sub.base_subscription_cents || 0)}
- |
-
- {formatPrice(sub.donation_cents || 0)}
- |
-
- {formatPrice(sub.amount_paid_cents || 0)}
- |
-
-
- {hasPermission('subscriptions.edit') && (
+ filteredSubscriptions.map((sub) => {
+ const isExpanded = expandedRows.has(sub.id);
+ return (
+
+
+ |
+
+ {sub.user.first_name} {sub.user.last_name}
+
+
+ {sub.user.email}
+
+ |
+
+
+ {sub.plan.name}
+
+
+ {sub.plan.billing_cycle}
+
+ |
+
+
+ {sub.status}
+
+ |
+
+
+ {formatDate(sub.start_date)}
+ to {formatDate(sub.end_date)}
+
+ |
+
+ {formatPrice(sub.base_subscription_cents || 0)}
+ |
+
+ {formatPrice(sub.donation_cents || 0)}
+ |
+
+ {formatPrice(sub.amount_paid_cents || 0)}
+ |
+
- )}
- {sub.status === 'active' && hasPermission('subscriptions.cancel') && (
-
- )}
-
- |
-
- ))
+ |
+
+
+ {hasPermission('subscriptions.edit') && (
+
+ )}
+ {sub.status === 'active' && hasPermission('subscriptions.cancel') && (
+
+ )}
+
+ |
+
+ {/* Expandable Details Row */}
+ {isExpanded && (
+
+
+
+
+ Transaction Details
+
+
+ {/* Payment Information */}
+
+
+
+ Payment Information
+
+
+ {sub.payment_completed_at && (
+
+ Payment Date:
+ {formatDateTime(sub.payment_completed_at)}
+
+ )}
+ {sub.payment_method && (
+
+ Payment Method:
+ {sub.payment_method}
+
+ )}
+ {sub.card_brand && sub.card_last4 && (
+
+ Card:
+ {sub.card_brand} ****{sub.card_last4}
+
+ )}
+
+
+
+ {/* Stripe Transaction IDs */}
+
+
+
+ Stripe Transaction IDs
+
+
+ {sub.stripe_payment_intent_id && (
+
+ Payment Intent:
+
+
+ {sub.stripe_payment_intent_id.substring(0, 20)}...
+
+
+
+
+ )}
+ {sub.stripe_charge_id && (
+
+ Charge ID:
+
+
+ {sub.stripe_charge_id.substring(0, 20)}...
+
+
+
+
+ )}
+ {sub.stripe_subscription_id && (
+
+ Subscription ID:
+
+
+ {sub.stripe_subscription_id.substring(0, 20)}...
+
+
+
+
+ )}
+ {sub.stripe_invoice_id && (
+
+ Invoice ID:
+
+
+ {sub.stripe_invoice_id.substring(0, 20)}...
+
+
+
+
+ )}
+ {sub.stripe_customer_id && (
+
+ Customer ID:
+
+
+ {sub.stripe_customer_id.substring(0, 20)}...
+
+
+
+
+ )}
+ {sub.stripe_receipt_url && (
+
+ Receipt:
+
+
+ )}
+
+
+
+
+ |
+
+ )}
+
+ );
+ })
) : (
- |
+ |
No subscriptions found
|