diff --git a/src/components/CreateSubscriptionDialog.js b/src/components/CreateSubscriptionDialog.js
new file mode 100644
index 0000000..9c4a5d5
--- /dev/null
+++ b/src/components/CreateSubscriptionDialog.js
@@ -0,0 +1,576 @@
+import React, { useState, useEffect } from 'react';
+import api from '../utils/api';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from './ui/dialog';
+import { Button } from './ui/button';
+import { Input } from './ui/input';
+import { Label } from './ui/label';
+import { Textarea } from './ui/textarea';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from './ui/select';
+import { Card } from './ui/card';
+import { toast } from 'sonner';
+import { Loader2, Repeat, Search, Calendar, Heart, X, User } from 'lucide-react';
+
+const CreateSubscriptionDialog = ({ open, onOpenChange, onSuccess }) => {
+ // Search state
+ const [searchQuery, setSearchQuery] = useState('');
+ const [searchResults, setSearchResults] = useState([]);
+ const [selectedUser, setSelectedUser] = useState(null);
+ const [searchLoading, setSearchLoading] = useState(false);
+ const [allUsers, setAllUsers] = useState([]);
+
+ // Plan state
+ const [plans, setPlans] = useState([]);
+ const [selectedPlan, setSelectedPlan] = useState(null);
+ const [useCustomPeriod, setUseCustomPeriod] = useState(false);
+
+ // Form state
+ const [formData, setFormData] = useState({
+ plan_id: '',
+ amount: '',
+ payment_date: new Date().toISOString().split('T')[0],
+ payment_method: 'cash',
+ custom_period_start: new Date().toISOString().split('T')[0],
+ custom_period_end: '',
+ notes: ''
+ });
+ const [loading, setLoading] = useState(false);
+
+ // Fetch users and plans when dialog opens
+ useEffect(() => {
+ const fetchData = async () => {
+ if (!open) return;
+
+ try {
+ const [usersResponse, plansResponse] = await Promise.all([
+ api.get('/admin/users'),
+ api.get('/admin/subscriptions/plans')
+ ]);
+ setAllUsers(usersResponse.data);
+ setPlans(plansResponse.data.filter(p => p.active));
+ } catch (error) {
+ toast.error('Failed to load data');
+ }
+ };
+
+ fetchData();
+ }, [open]);
+
+ // Filter users based on search query
+ useEffect(() => {
+ if (!searchQuery.trim()) {
+ setSearchResults([]);
+ return;
+ }
+
+ setSearchLoading(true);
+ const query = searchQuery.toLowerCase();
+ const filtered = allUsers.filter(user =>
+ user.first_name?.toLowerCase().includes(query) ||
+ user.last_name?.toLowerCase().includes(query) ||
+ user.email?.toLowerCase().includes(query)
+ ).slice(0, 10); // Limit to 10 results
+
+ setSearchResults(filtered);
+ setSearchLoading(false);
+ }, [searchQuery, allUsers]);
+
+ // Update amount when plan changes
+ useEffect(() => {
+ if (selectedPlan && !formData.amount) {
+ const suggestedAmount = (selectedPlan.suggested_price_cents || selectedPlan.minimum_price_cents || selectedPlan.price_cents) / 100;
+ setFormData(prev => ({
+ ...prev,
+ amount: suggestedAmount.toFixed(2)
+ }));
+ }
+ }, [selectedPlan]);
+
+ // Calculate donation breakdown
+ const getAmountBreakdown = () => {
+ if (!selectedPlan || !formData.amount) return null;
+
+ const totalCents = Math.round(parseFloat(formData.amount) * 100);
+ const minimumCents = selectedPlan.minimum_price_cents || selectedPlan.price_cents || 3000;
+ const donationCents = Math.max(0, totalCents - minimumCents);
+
+ return {
+ total: totalCents,
+ base: minimumCents,
+ donation: donationCents
+ };
+ };
+
+ const formatPrice = (cents) => {
+ return `$${(cents / 100).toFixed(2)}`;
+ };
+
+ const breakdown = getAmountBreakdown();
+
+ const handleSelectUser = (user) => {
+ setSelectedUser(user);
+ setSearchQuery('');
+ setSearchResults([]);
+ };
+
+ const handleClearUser = () => {
+ setSelectedUser(null);
+ setFormData({
+ plan_id: '',
+ amount: '',
+ payment_date: new Date().toISOString().split('T')[0],
+ payment_method: 'cash',
+ custom_period_start: new Date().toISOString().split('T')[0],
+ custom_period_end: '',
+ notes: ''
+ });
+ setSelectedPlan(null);
+ setUseCustomPeriod(false);
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ if (!selectedUser) {
+ toast.error('Please select a user');
+ return;
+ }
+
+ if (!formData.plan_id) {
+ toast.error('Please select a subscription plan');
+ return;
+ }
+
+ if (!formData.amount || parseFloat(formData.amount) <= 0) {
+ toast.error('Please enter a valid payment amount');
+ return;
+ }
+
+ // Validate minimum amount
+ const amountCents = Math.round(parseFloat(formData.amount) * 100);
+ const minimumCents = selectedPlan.minimum_price_cents || selectedPlan.price_cents || 3000;
+ if (amountCents < minimumCents) {
+ toast.error(`Amount must be at least ${formatPrice(minimumCents)}`);
+ return;
+ }
+
+ if (useCustomPeriod && (!formData.custom_period_start || !formData.custom_period_end)) {
+ toast.error('Please specify both start and end dates for custom period');
+ return;
+ }
+
+ setLoading(true);
+
+ try {
+ const payload = {
+ plan_id: formData.plan_id,
+ amount_cents: amountCents,
+ payment_date: new Date(formData.payment_date).toISOString(),
+ payment_method: formData.payment_method,
+ override_plan_dates: useCustomPeriod,
+ notes: formData.notes || null
+ };
+
+ if (useCustomPeriod) {
+ payload.custom_period_start = new Date(formData.custom_period_start).toISOString();
+ payload.custom_period_end = new Date(formData.custom_period_end).toISOString();
+ }
+
+ await api.post(`/admin/users/${selectedUser.id}/activate-payment`, payload);
+ toast.success(`Subscription created for ${selectedUser.first_name} ${selectedUser.last_name}!`);
+
+ // Reset form
+ handleClearUser();
+ onOpenChange(false);
+ if (onSuccess) onSuccess();
+ } catch (error) {
+ const errorMessage = error.response?.data?.detail || 'Failed to create subscription';
+ toast.error(errorMessage);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleClose = () => {
+ handleClearUser();
+ setSearchQuery('');
+ setSearchResults([]);
+ onOpenChange(false);
+ };
+
+ return (
+
+ );
+};
+
+export default CreateSubscriptionDialog;
diff --git a/src/components/ViewRegistrationDialog.js b/src/components/ViewRegistrationDialog.js
new file mode 100644
index 0000000..8f02dcf
--- /dev/null
+++ b/src/components/ViewRegistrationDialog.js
@@ -0,0 +1,172 @@
+import React from 'react';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from './ui/dialog';
+import { Button } from './ui/button';
+import { Card } from './ui/card';
+import { User, Mail, Phone, Calendar, UserCheck, Clock, FileText } from 'lucide-react';
+import StatusBadge from './StatusBadge';
+
+const ViewRegistrationDialog = ({ open, onOpenChange, user }) => {
+ if (!user) return null;
+
+ const formatDate = (dateString) => {
+ if (!dateString) return '—';
+ return new Date(dateString).toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ });
+ };
+
+ const formatDateTime = (dateString) => {
+ if (!dateString) return '—';
+ return new Date(dateString).toLocaleString('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ });
+ };
+
+ const formatPhoneNumber = (phone) => {
+ if (!phone) return '—';
+ const cleaned = phone.replace(/\D/g, '');
+ if (cleaned.length === 10) {
+ return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3, 6)}-${cleaned.slice(6)}`;
+ }
+ return phone;
+ };
+
+ const InfoRow = ({ icon: Icon, label, value }) => (
+
+
+
+
+
+
+ {label}
+
+
+ {value || '—'}
+
+
+
+ );
+
+ return (
+
+ );
+};
+
+export default ViewRegistrationDialog;
diff --git a/src/pages/admin/AdminSubscriptions.js b/src/pages/admin/AdminSubscriptions.js
index e6ed2ec..7915282 100644
--- a/src/pages/admin/AdminSubscriptions.js
+++ b/src/pages/admin/AdminSubscriptions.js
@@ -38,7 +38,8 @@ import {
ChevronDown,
ChevronUp,
ExternalLink,
- Copy
+ Copy,
+ Repeat
} from 'lucide-react';
import {
DropdownMenu,
@@ -47,6 +48,7 @@ import {
DropdownMenuTrigger,
} from '../../components/ui/dropdown-menu';
import StatusBadge from '@/components/StatusBadge';
+import CreateSubscriptionDialog from '@/components/CreateSubscriptionDialog';
const AdminSubscriptions = () => {
const { hasPermission } = useAuth();
@@ -61,6 +63,9 @@ const AdminSubscriptions = () => {
const [exporting, setExporting] = useState(false);
const [expandedRows, setExpandedRows] = useState(new Set());
+ //create subsdcription dialog state
+ const [createDialogOpen, setCreateDialogOpen] = useState(false);
+
// Edit subscription dialog state
const [editDialogOpen, setEditDialogOpen] = useState(false);
const [selectedSubscription, setSelectedSubscription] = useState(null);
@@ -313,671 +318,691 @@ Proceed with activation?`;
}
return (
-
- {/* Header */}
-
-
- Subscription Management
-
-
- View and manage all member subscriptions
-
-
+ <>
+
+ {/* Header */}
+
- {/* Stats Cards */}
-
-
-
-
-
- Total Subscriptions
-
-
- {stats.total || 0}
-
-
-
-
-
-
-
-
-
-
-
-
- Active Members
-
-
- {stats.active || 0}
-
-
-
-
-
-
-
-
-
-
-
-
- Total Revenue
-
-
- {formatPrice(stats.total_revenue || 0)}
-
-
-
-
-
-
-
-
-
-
-
-
- Total Donations
-
-
- {formatPrice(stats.total_donations || 0)}
-
-
-
-
-
-
-
-
-
- {/* Search & Filter Bar */}
-
-
- {/* Search */}
-
-
-
- setSearchQuery(e.target.value)}
- className="pl-10 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
- />
-
-
-
- {/* Status Filter */}
-
+
+ Subscription Management
+
+
+ View and manage all member subscriptions
+
-
- {/* Plan Filter */}
-
-
-
-
-
-
-
- Showing {filteredSubscriptions.length} of {subscriptions.length} subscriptions
-
-
- {/* Export Dropdown */}
- {hasPermission('subscriptions.export') && (
-
-
-
-
-
- handleExport('all')}
- className="cursor-pointer hover:bg-[var(--lavender-300)] rounded-lg p-3"
- >
-
- Export All Subscriptions
-
- handleExport('current')}
- className="cursor-pointer hover:bg-[var(--lavender-300)] rounded-lg p-3"
- >
-
- Export Current View
-
-
-
- )}
-
-
-
- {/* Subscriptions Table */}
-
- {/* Mobile Card View */}
-
- {filteredSubscriptions.length > 0 ? (
- filteredSubscriptions.map((sub) => (
-
-
- {/* Member Info */}
-
-
-
- {sub.user.first_name} {sub.user.last_name}
-
-
- {sub.user.email}
-
-
-
-
-
- {/* Plan & Period */}
-
-
-
Plan
-
{sub.plan.name}
-
{sub.plan.billing_cycle}
-
-
-
Period
-
- {new Date(sub.current_period_start).toLocaleDateString()} -
- {new Date(sub.current_period_end).toLocaleDateString()}
-
-
-
-
- {/* Pricing */}
-
-
-
Base Fee
-
- ${(sub.base_fee_cents / 100).toFixed(2)}
-
-
-
-
Donation
-
- ${(sub.donation_cents / 100).toFixed(2)}
-
-
-
-
Total
-
- ${(sub.total_cents / 100).toFixed(2)}
-
-
-
-
- {/* Actions */}
-
- {hasPermission('subscriptions.edit') && (
-
- )}
- {sub.status === 'active' && hasPermission('subscriptions.cancel') && (
-
- )}
-
-
-
- ))
- ) : (
-
- No subscriptions found
-
+ {hasPermission('users.create') && (
+
)}
- {/* Desktop Table View */}
-
-
-
-
- |
- Member
- |
-
- Plan
- |
-
- Status
- |
-
- Period
- |
-
- Base Fee
- |
-
- Donation
- |
-
- Total
- |
-
- Details
- |
-
- Actions
- |
-
-
-
- {filteredSubscriptions.length > 0 ? (
- 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}
-
- |
-
-
+ {/* Stats Cards */}
+
+
+
+
+
+ Total Subscriptions
+
+
+ {stats.total || 0}
+
+
+
+
+
+
+
- |
-
-
- {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') && (
-
- )}
- {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}
-
- )}
-
-
+
+
+
+
+ Active Members
+
+
+ {stats.active || 0}
+
+
+
+
+
+
+
- {/* 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
- |
-
- )}
-
-
+
+
+
+
+ Total Revenue
+
+
+ {formatPrice(stats.total_revenue || 0)}
+
+
+
+
+
+
+
+
+
+
+
+
+ Total Donations
+
+
+ {formatPrice(stats.total_donations || 0)}
+
+
+
+
+
+
+
-
- {/* Edit Subscription Dialog */}
-