348 lines
13 KiB
JavaScript
348 lines
13 KiB
JavaScript
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 TableCell = ({ align = 'text-left', className = '', style, children, ...props }) => (
|
|
<td
|
|
className={`p-4 ${align} ${className}`.trim()}
|
|
style={style}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</td>
|
|
);
|
|
|
|
|
|
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">
|
|
<TableCell>
|
|
<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>
|
|
</TableCell>
|
|
<TableCell>
|
|
<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>
|
|
</TableCell>
|
|
<TableCell>
|
|
<StatusBadge status={sub.status} />
|
|
</TableCell>
|
|
<TableCell>
|
|
<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>
|
|
</TableCell>
|
|
<TableCell
|
|
align="text-right"
|
|
className="text-[var(--purple-ink)]"
|
|
style={{ fontFamily: "'Inter', sans-serif" }}
|
|
>
|
|
{formatPrice(sub.base_subscription_cents || 0)}
|
|
</TableCell>
|
|
<TableCell
|
|
align="text-right"
|
|
className="text-[var(--orange-light)]"
|
|
style={{ fontFamily: "'Inter', sans-serif" }}
|
|
>
|
|
{formatPrice(sub.donation_cents || 0)}
|
|
</TableCell>
|
|
<TableCell
|
|
align="text-right"
|
|
className="font-semibold text-[var(--purple-ink)]"
|
|
style={{ fontFamily: "'Inter', sans-serif" }}
|
|
>
|
|
{formatPrice(sub.amount_paid_cents || 0)}
|
|
</TableCell>
|
|
<TableCell>
|
|
<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>
|
|
</TableCell>
|
|
<TableCell>
|
|
<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>
|
|
</TableCell>
|
|
</tr>
|
|
|
|
{isExpanded && (
|
|
<tr className="bg-[var(--lavender-400)]/30">
|
|
<TableCell 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>
|
|
</TableCell>
|
|
</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>
|
|
<TableCell
|
|
align="text-center"
|
|
className="p-12 text-brand-purple "
|
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
|
colSpan={9}
|
|
>
|
|
No subscriptions found
|
|
</TableCell>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
);
|
|
|
|
export default SubscriptionsTable;
|