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 */} +
+ +