Componentized subscription table
This commit is contained in:
319
src/components/admin/SubscriptionsTable.jsx
Normal file
319
src/components/admin/SubscriptionsTable.jsx
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
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 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">
|
||||||
|
<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={onToggle}
|
||||||
|
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={() => 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>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{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">
|
||||||
|
<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>
|
||||||
|
</td>
|
||||||
|
</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>
|
||||||
|
<td colSpan={9} className="p-12 text-center text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
No subscriptions found
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default SubscriptionsTable;
|
||||||
@@ -35,11 +35,14 @@ import {
|
|||||||
FileDown,
|
FileDown,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
Info,
|
Info,
|
||||||
|
Repeat,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
Copy,
|
Copy
|
||||||
Repeat
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -49,6 +52,7 @@ import {
|
|||||||
} from '../../components/ui/dropdown-menu';
|
} from '../../components/ui/dropdown-menu';
|
||||||
import StatusBadge from '@/components/StatusBadge';
|
import StatusBadge from '@/components/StatusBadge';
|
||||||
import CreateSubscriptionDialog from '@/components/CreateSubscriptionDialog';
|
import CreateSubscriptionDialog from '@/components/CreateSubscriptionDialog';
|
||||||
|
import SubscriptionsTable from '@/components/admin/SubscriptionsTable';
|
||||||
|
|
||||||
const AdminSubscriptions = () => {
|
const AdminSubscriptions = () => {
|
||||||
const { hasPermission } = useAuth();
|
const { hasPermission } = useAuth();
|
||||||
@@ -590,277 +594,18 @@ Proceed with activation?`;
|
|||||||
|
|
||||||
{/* Desktop Table View */}
|
{/* Desktop Table View */}
|
||||||
<div className="hidden md:block overflow-x-auto">
|
<div className="hidden md:block overflow-x-auto">
|
||||||
<table className="w-full">
|
<SubscriptionsTable
|
||||||
<thead>
|
subscriptions={filteredSubscriptions}
|
||||||
<tr className="bg-[var(--neutral-800)]/20 border-b border-[var(--neutral-800)]">
|
expandedRows={expandedRows}
|
||||||
<th className="text-left p-4 text-[var(--purple-ink)] font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
|
onToggleRowExpansion={toggleRowExpansion}
|
||||||
Member
|
onEdit={handleEdit}
|
||||||
</th>
|
onCancel={handleCancelSubscription}
|
||||||
<th className="text-left p-4 text-[var(--purple-ink)] font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
|
hasPermission={hasPermission}
|
||||||
Plan
|
formatDate={formatDate}
|
||||||
</th>
|
formatDateTime={formatDateTime}
|
||||||
<th className="text-left p-4 text-[var(--purple-ink)] font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
|
formatPrice={formatPrice}
|
||||||
Status
|
copyToClipboard={copyToClipboard}
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user