From d638afcdb2d78a4c329f7bf41305d0b31b24a15c Mon Sep 17 00:00:00 2001 From: kayela Date: Wed, 28 Jan 2026 18:59:19 -0600 Subject: [PATCH] add column for email expiry date Members > Invite member says invite Staff in dialog resend email button Update form member form to say member and not staff review application function manual payment functionality basic implementation of theme actions dropdown --- src/components/CreateSubscriptionDialog.js | 576 +++++++++ src/components/ViewRegistrationDialog.js | 172 +++ src/pages/admin/AdminSubscriptions.js | 1309 ++++++++++---------- src/pages/admin/AdminValidations.js | 215 ++-- 4 files changed, 1535 insertions(+), 737 deletions(-) create mode 100644 src/components/CreateSubscriptionDialog.js create mode 100644 src/components/ViewRegistrationDialog.js 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 ( + + + + + + Create Subscription + + + Search for an existing member and create a subscription with manual payment processing. + + + +
+
+ {/* User Search Section */} + {!selectedUser ? ( +
+ +
+ + setSearchQuery(e.target.value)} + className="pl-10 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple" + /> + {searchLoading && ( + + )} +
+ + {/* Search Results */} + {searchResults.length > 0 && ( + +
+ {searchResults.map((user) => ( + + ))} +
+
+ )} + + {searchQuery && !searchLoading && searchResults.length === 0 && ( +

+ No members found matching "{searchQuery}" +

+ )} +
+ ) : ( + /* Selected User Card */ + +
+
+
+ +
+
+

+ {selectedUser.first_name} {selectedUser.last_name} +

+

+ {selectedUser.email} +

+
+
+ +
+
+ )} + + {/* Payment Form - Only show when user is selected */} + {selectedUser && ( + <> + {/* Plan Selection */} +
+ + + {selectedPlan && ( +

+ {selectedPlan.description || `${selectedPlan.billing_cycle} subscription`} +

+ )} +
+ + {/* Payment Amount */} +
+ + setFormData({ ...formData, amount: e.target.value })} + className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple" + required + /> + {selectedPlan && ( +

+ Minimum: {formatPrice(selectedPlan.minimum_price_cents || selectedPlan.price_cents || 3000)} +

+ )} +
+ + {/* Amount Breakdown */} + {breakdown && breakdown.total >= breakdown.base && ( + +
+
+ Membership Fee: + {formatPrice(breakdown.base)} +
+ {breakdown.donation > 0 && ( +
+ + + Additional Donation: + + {formatPrice(breakdown.donation)} +
+ )} +
+ Total: + {formatPrice(breakdown.total)} +
+
+
+ )} + + {/* Payment Date */} +
+ +
+ + setFormData({ ...formData, payment_date: e.target.value })} + className="pl-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple" + required + /> +
+
+ + {/* Payment Method */} +
+ + +
+ + {/* Subscription Period */} +
+ + +
+ setUseCustomPeriod(e.target.checked)} + className="rounded border-[var(--neutral-800)]" + /> + +
+ + {useCustomPeriod ? ( +
+
+ + setFormData({ ...formData, custom_period_start: e.target.value })} + className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple" + required={useCustomPeriod} + /> +
+
+ + setFormData({ ...formData, custom_period_end: e.target.value })} + className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple" + required={useCustomPeriod} + /> +
+
+ ) : ( + selectedPlan && ( +
+ {selectedPlan.custom_cycle_enabled ? ( + <> +

+ Plan uses custom billing cycle: +
+ {(() => { + const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + const startMonth = months[(selectedPlan.custom_cycle_start_month || 1) - 1]; + const endMonth = months[(selectedPlan.custom_cycle_end_month || 12) - 1]; + return `${startMonth} ${selectedPlan.custom_cycle_start_day} - ${endMonth} ${selectedPlan.custom_cycle_end_day} (recurring annually)`; + })()} +

+

+ Subscription will end on the upcoming cycle end date based on today's date. +

+ + ) : ( +

+ Will use plan's billing cycle: {selectedPlan.billing_cycle} +
+ Starts today, ends {selectedPlan.billing_cycle === 'monthly' ? '30 days' : + selectedPlan.billing_cycle === 'quarterly' ? '90 days' : + selectedPlan.billing_cycle === 'yearly' ? '1 year' : + selectedPlan.billing_cycle === 'lifetime' ? 'lifetime' : '1 year'} from now +

+ )} +
+ ) + )} +
+ + {/* Notes */} +
+ +