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
This commit is contained in:
576
src/components/CreateSubscriptionDialog.js
Normal file
576
src/components/CreateSubscriptionDialog.js
Normal file
@@ -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 (
|
||||||
|
<Dialog open={open} onOpenChange={handleClose}>
|
||||||
|
<DialogContent className="sm:max-w-[700px] rounded-2xl max-h-[90vh] overflow-y-auto scrollbar-dashboard">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-2xl text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
<Repeat className="h-6 w-6" />
|
||||||
|
Create Subscription
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
Search for an existing member and create a subscription with manual payment processing.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="grid gap-6 py-4">
|
||||||
|
{/* User Search Section */}
|
||||||
|
{!selectedUser ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Search Member
|
||||||
|
</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-brand-purple" />
|
||||||
|
<Input
|
||||||
|
placeholder="Search by name or email..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="pl-10 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
|
||||||
|
/>
|
||||||
|
{searchLoading && (
|
||||||
|
<Loader2 className="absolute right-3 top-1/2 transform -translate-y-1/2 h-4 w-4 animate-spin text-brand-purple" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search Results */}
|
||||||
|
{searchResults.length > 0 && (
|
||||||
|
<Card className="border-2 border-[var(--neutral-800)] rounded-xl overflow-hidden">
|
||||||
|
<div className="max-h-60 overflow-y-auto">
|
||||||
|
{searchResults.map((user) => (
|
||||||
|
<button
|
||||||
|
key={user.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleSelectUser(user)}
|
||||||
|
className="w-full p-3 text-left hover:bg-[var(--lavender-400)] transition-colors border-b border-[var(--neutral-800)] last:border-b-0"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="h-10 w-10 rounded-full bg-[var(--neutral-800)]/20 flex items-center justify-center">
|
||||||
|
<User className="h-5 w-5 text-brand-purple" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
{user.first_name} {user.last_name}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
{user.email}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{searchQuery && !searchLoading && searchResults.length === 0 && (
|
||||||
|
<p className="text-sm text-brand-purple text-center py-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
No members found matching "{searchQuery}"
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
/* Selected User Card */
|
||||||
|
<Card className="p-4 bg-[var(--lavender-400)] border-2 border-[var(--neutral-800)] rounded-xl">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="h-12 w-12 rounded-full bg-[var(--neutral-800)]/20 flex items-center justify-center">
|
||||||
|
<User className="h-6 w-6 text-brand-purple" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
{selectedUser.first_name} {selectedUser.last_name}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
{selectedUser.email}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleClearUser}
|
||||||
|
className="text-brand-purple hover:bg-[var(--neutral-800)]/20"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Payment Form - Only show when user is selected */}
|
||||||
|
{selectedUser && (
|
||||||
|
<>
|
||||||
|
{/* Plan Selection */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="plan_id" className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Subscription Plan
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={formData.plan_id}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const plan = plans.find(p => p.id === value);
|
||||||
|
setSelectedPlan(plan);
|
||||||
|
const suggestedAmount = plan ? (plan.suggested_price_cents || plan.minimum_price_cents || plan.price_cents) / 100 : '';
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
plan_id: value,
|
||||||
|
amount: suggestedAmount ? suggestedAmount.toFixed(2) : ''
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="rounded-xl border-2 border-[var(--neutral-800)]">
|
||||||
|
<SelectValue placeholder="Select subscription plan" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{plans.map(plan => {
|
||||||
|
const minPrice = (plan.minimum_price_cents || plan.price_cents) / 100;
|
||||||
|
const sugPrice = plan.suggested_price_cents ? (plan.suggested_price_cents / 100) : null;
|
||||||
|
return (
|
||||||
|
<SelectItem key={plan.id} value={plan.id}>
|
||||||
|
{plan.name} - ${minPrice.toFixed(2)}{sugPrice && sugPrice > minPrice ? ` (Suggested: $${sugPrice.toFixed(2)})` : ''}/{plan.billing_cycle}
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{selectedPlan && (
|
||||||
|
<p className="text-xs text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
{selectedPlan.description || `${selectedPlan.billing_cycle} subscription`}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Payment Amount */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="amount" className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Payment Amount ($)
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="amount"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
min="0"
|
||||||
|
placeholder="Enter amount"
|
||||||
|
value={formData.amount}
|
||||||
|
onChange={(e) => setFormData({ ...formData, amount: e.target.value })}
|
||||||
|
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{selectedPlan && (
|
||||||
|
<p className="text-xs text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
Minimum: {formatPrice(selectedPlan.minimum_price_cents || selectedPlan.price_cents || 3000)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Amount Breakdown */}
|
||||||
|
{breakdown && breakdown.total >= breakdown.base && (
|
||||||
|
<Card className="p-4 bg-[var(--lavender-400)] border border-[var(--neutral-800)]">
|
||||||
|
<div className="space-y-2 text-sm" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
<div className="flex justify-between text-[var(--purple-ink)]">
|
||||||
|
<span>Membership Fee:</span>
|
||||||
|
<span className="font-semibold">{formatPrice(breakdown.base)}</span>
|
||||||
|
</div>
|
||||||
|
{breakdown.donation > 0 && (
|
||||||
|
<div className="flex justify-between text-[var(--orange-light)]">
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Heart className="h-4 w-4" />
|
||||||
|
Additional Donation:
|
||||||
|
</span>
|
||||||
|
<span className="font-semibold">{formatPrice(breakdown.donation)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex justify-between text-[var(--purple-ink)] font-bold text-base pt-2 border-t border-[var(--neutral-800)]">
|
||||||
|
<span>Total:</span>
|
||||||
|
<span>{formatPrice(breakdown.total)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Payment Date */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="payment_date" className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Payment Date
|
||||||
|
</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Calendar className="absolute left-4 top-1/2 transform -translate-y-1/2 h-5 w-5 text-brand-purple" />
|
||||||
|
<Input
|
||||||
|
id="payment_date"
|
||||||
|
type="date"
|
||||||
|
value={formData.payment_date}
|
||||||
|
onChange={(e) => setFormData({ ...formData, payment_date: e.target.value })}
|
||||||
|
className="pl-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Payment Method */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="payment_method" className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Payment Method
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={formData.payment_method}
|
||||||
|
onValueChange={(value) => setFormData({ ...formData, payment_method: value })}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="rounded-xl border-2 border-[var(--neutral-800)]">
|
||||||
|
<SelectValue placeholder="Select payment method" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="cash">Cash</SelectItem>
|
||||||
|
<SelectItem value="bank_transfer">Bank Transfer</SelectItem>
|
||||||
|
<SelectItem value="check">Check</SelectItem>
|
||||||
|
<SelectItem value="other">Other</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Subscription Period */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Subscription Period
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="use_custom_period"
|
||||||
|
checked={useCustomPeriod}
|
||||||
|
onChange={(e) => setUseCustomPeriod(e.target.checked)}
|
||||||
|
className="rounded border-[var(--neutral-800)]"
|
||||||
|
/>
|
||||||
|
<Label htmlFor="use_custom_period" className="text-sm text-brand-purple font-normal cursor-pointer" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
Use custom dates instead of plan's billing cycle
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{useCustomPeriod ? (
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="custom_period_start" className="text-sm text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Start Date
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="custom_period_start"
|
||||||
|
type="date"
|
||||||
|
value={formData.custom_period_start}
|
||||||
|
onChange={(e) => setFormData({ ...formData, custom_period_start: e.target.value })}
|
||||||
|
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
|
||||||
|
required={useCustomPeriod}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="custom_period_end" className="text-sm text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
End Date
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="custom_period_end"
|
||||||
|
type="date"
|
||||||
|
value={formData.custom_period_end}
|
||||||
|
onChange={(e) => setFormData({ ...formData, custom_period_end: e.target.value })}
|
||||||
|
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
|
||||||
|
required={useCustomPeriod}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
selectedPlan && (
|
||||||
|
<div className="text-sm text-brand-purple bg-[var(--lavender-300)] p-3 rounded-lg space-y-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
{selectedPlan.custom_cycle_enabled ? (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
<span className="font-medium text-[var(--purple-ink)]">Plan uses custom billing cycle:</span>
|
||||||
|
<br />
|
||||||
|
{(() => {
|
||||||
|
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)`;
|
||||||
|
})()}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs">
|
||||||
|
Subscription will end on the upcoming cycle end date based on today's date.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p>
|
||||||
|
Will use plan's billing cycle: <span className="font-medium">{selectedPlan.billing_cycle}</span>
|
||||||
|
<br />
|
||||||
|
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
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Notes */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="notes" className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Notes (Optional)
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id="notes"
|
||||||
|
placeholder="Additional notes about the payment..."
|
||||||
|
value={formData.notes}
|
||||||
|
onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
|
||||||
|
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple min-h-[100px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleClose}
|
||||||
|
className="rounded-xl"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="rounded-xl bg-[var(--green-light)] hover:bg-[var(--green-fern)] text-white"
|
||||||
|
disabled={loading || !selectedUser}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||||
|
Creating...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Repeat className="h-4 w-4 mr-2" />
|
||||||
|
Create Subscription
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateSubscriptionDialog;
|
||||||
172
src/components/ViewRegistrationDialog.js
Normal file
172
src/components/ViewRegistrationDialog.js
Normal file
@@ -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 }) => (
|
||||||
|
<div className="flex items-start gap-3 py-3 border-b border-[var(--neutral-800)] last:border-b-0">
|
||||||
|
<div className="h-10 w-10 rounded-lg bg-[var(--lavender-400)] flex items-center justify-center flex-shrink-0">
|
||||||
|
<Icon className="h-5 w-5 text-brand-purple" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
{label}
|
||||||
|
</p>
|
||||||
|
<p className="font-medium text-[var(--purple-ink)] break-words" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
{value || '—'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="sm:max-w-[600px] rounded-2xl max-h-[90vh] overflow-y-auto scrollbar-dashboard">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-2xl text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
<FileText className="h-6 w-6" />
|
||||||
|
Registration Details
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
View the registration information for this member application.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="py-4 space-y-4">
|
||||||
|
{/* User Header Card */}
|
||||||
|
<Card className="p-4 bg-[var(--lavender-400)] border-2 border-[var(--neutral-800)] rounded-xl">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="h-16 w-16 rounded-full bg-[var(--neutral-800)]/20 flex items-center justify-center">
|
||||||
|
<User className="h-8 w-8 text-brand-purple" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
{user.first_name} {user.last_name}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
{user.email}
|
||||||
|
</p>
|
||||||
|
<div className="mt-2">
|
||||||
|
<StatusBadge status={user.status} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Contact Information */}
|
||||||
|
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
|
||||||
|
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Contact Information
|
||||||
|
</h3>
|
||||||
|
<InfoRow icon={Mail} label="Email Address" value={user.email} />
|
||||||
|
<InfoRow icon={Phone} label="Phone Number" value={formatPhoneNumber(user.phone)} />
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Registration Details */}
|
||||||
|
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
|
||||||
|
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Registration Details
|
||||||
|
</h3>
|
||||||
|
<InfoRow icon={Calendar} label="Registration Date" value={formatDate(user.created_at)} />
|
||||||
|
<InfoRow icon={UserCheck} label="Referred By" value={user.referred_by_member_name} />
|
||||||
|
<InfoRow icon={Clock} label="Email Verification Expires" value={formatDateTime(user.email_verification_expires_at)} />
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Additional Information (if available) */}
|
||||||
|
{(user.address || user.city || user.state || user.zip_code) && (
|
||||||
|
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
|
||||||
|
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Address
|
||||||
|
</h3>
|
||||||
|
<div className="text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
{user.address && <p>{user.address}</p>}
|
||||||
|
{(user.city || user.state || user.zip_code) && (
|
||||||
|
<p>
|
||||||
|
{[user.city, user.state, user.zip_code].filter(Boolean).join(', ')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Notes (if available) */}
|
||||||
|
{user.notes && (
|
||||||
|
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
|
||||||
|
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Notes
|
||||||
|
</h3>
|
||||||
|
<p className="text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
{user.notes}
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Rejection Reason (if rejected) */}
|
||||||
|
{user.status === 'rejected' && user.rejection_reason && (
|
||||||
|
<Card className="p-4 border border-red-300 bg-red-50 dark:bg-red-500/10 rounded-xl">
|
||||||
|
<h3 className="text-lg font-semibold text-red-600 mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Rejection Reason
|
||||||
|
</h3>
|
||||||
|
<p className="text-red-600" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
{user.rejection_reason}
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
className="rounded-xl bg-[var(--purple-ink)] hover:bg-[var(--purple-ink)]/90 text-white"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ViewRegistrationDialog;
|
||||||
@@ -38,7 +38,8 @@ import {
|
|||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
Copy
|
Copy,
|
||||||
|
Repeat
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -47,6 +48,7 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} 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';
|
||||||
|
|
||||||
const AdminSubscriptions = () => {
|
const AdminSubscriptions = () => {
|
||||||
const { hasPermission } = useAuth();
|
const { hasPermission } = useAuth();
|
||||||
@@ -61,6 +63,9 @@ const AdminSubscriptions = () => {
|
|||||||
const [exporting, setExporting] = useState(false);
|
const [exporting, setExporting] = useState(false);
|
||||||
const [expandedRows, setExpandedRows] = useState(new Set());
|
const [expandedRows, setExpandedRows] = useState(new Set());
|
||||||
|
|
||||||
|
//create subsdcription dialog state
|
||||||
|
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
||||||
|
|
||||||
// Edit subscription dialog state
|
// Edit subscription dialog state
|
||||||
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||||
const [selectedSubscription, setSelectedSubscription] = useState(null);
|
const [selectedSubscription, setSelectedSubscription] = useState(null);
|
||||||
@@ -313,8 +318,11 @@ Proceed with activation?`;
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
|
<div className='flex justify-between'>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<h1 className="text-3xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
Subscription Management
|
Subscription Management
|
||||||
@@ -323,6 +331,16 @@ Proceed with activation?`;
|
|||||||
View and manage all member subscriptions
|
View and manage all member subscriptions
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{hasPermission('users.create') && (
|
||||||
|
<Button
|
||||||
|
onClick={() => setCreateDialogOpen(true)}
|
||||||
|
className="btn-util-green "
|
||||||
|
>
|
||||||
|
<Repeat className="h-5 w-5 mr-2" />
|
||||||
|
Create Subscription
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Stats Cards */}
|
{/* Stats Cards */}
|
||||||
<div className="grid md:grid-cols-4 gap-6">
|
<div className="grid md:grid-cols-4 gap-6">
|
||||||
@@ -978,6 +996,13 @@ Proceed with activation?`;
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
<CreateSubscriptionDialog
|
||||||
|
open={createDialogOpen}
|
||||||
|
onOpenChange={setCreateDialogOpen}
|
||||||
|
onSuccess={fetchData}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import { useAuth } from '../../context/AuthContext';
|
import { useAuth } from '../../context/AuthContext';
|
||||||
import api from '../../utils/api';
|
import api from '../../utils/api';
|
||||||
import { Card } from '../../components/ui/card';
|
import { Card } from '../../components/ui/card';
|
||||||
import { Button } from '../../components/ui/button';
|
|
||||||
import { Input } from '../../components/ui/input';
|
import { Input } from '../../components/ui/input';
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
@@ -29,12 +28,14 @@ import {
|
|||||||
PaginationEllipsis,
|
PaginationEllipsis,
|
||||||
} from '../../components/ui/pagination';
|
} from '../../components/ui/pagination';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { CheckCircle, Clock, Search, ArrowUp, ArrowDown, X, XCircle } from 'lucide-react';
|
import { CheckCircle, Clock, Search, ArrowUp, ArrowDown, X, FileText, XCircle } from 'lucide-react';
|
||||||
import PaymentActivationDialog from '../../components/PaymentActivationDialog';
|
import PaymentActivationDialog from '../../components/PaymentActivationDialog';
|
||||||
import ConfirmationDialog from '../../components/ConfirmationDialog';
|
import ConfirmationDialog from '../../components/ConfirmationDialog';
|
||||||
import RejectionDialog from '../../components/RejectionDialog';
|
import RejectionDialog from '../../components/RejectionDialog';
|
||||||
import StatusBadge from '@/components/StatusBadge';
|
import StatusBadge from '@/components/StatusBadge';
|
||||||
import { StatCard } from '@/components/StatCard';
|
import { StatCard } from '@/components/StatCard';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import ViewRegistrationDialog from '@/components/ViewRegistrationDialog';
|
||||||
|
|
||||||
const AdminValidations = () => {
|
const AdminValidations = () => {
|
||||||
const { hasPermission } = useAuth();
|
const { hasPermission } = useAuth();
|
||||||
@@ -48,6 +49,8 @@ const AdminValidations = () => {
|
|||||||
const [pendingAction, setPendingAction] = useState(null);
|
const [pendingAction, setPendingAction] = useState(null);
|
||||||
const [rejectionDialogOpen, setRejectionDialogOpen] = useState(false);
|
const [rejectionDialogOpen, setRejectionDialogOpen] = useState(false);
|
||||||
const [userToReject, setUserToReject] = useState(null);
|
const [userToReject, setUserToReject] = useState(null);
|
||||||
|
const [viewRegistrationDialogOpen, setViewRegistrationDialogOpen] = useState(false);
|
||||||
|
const [selectedUserForView, setSelectedUserForView] = useState(null);
|
||||||
|
|
||||||
// Filtering state
|
// Filtering state
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
@@ -239,7 +242,10 @@ const AdminValidations = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRegistrationDialog = (user) => {
|
||||||
|
setSelectedUserForView(user);
|
||||||
|
setViewRegistrationDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
// Resend Email Handler
|
// Resend Email Handler
|
||||||
const handleResendVerification = async (user) => {
|
const handleResendVerification = async (user) => {
|
||||||
@@ -279,6 +285,37 @@ const AdminValidations = () => {
|
|||||||
<ArrowDown className="h-4 w-4 inline ml-1" />;
|
<ArrowDown className="h-4 w-4 inline ml-1" />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 handleActionSelect = (user, action) => {
|
||||||
|
switch (action) {
|
||||||
|
case 'validate':
|
||||||
|
handleValidateRequest(user);
|
||||||
|
break;
|
||||||
|
case 'bypass_validate':
|
||||||
|
handleBypassAndValidateRequest(user);
|
||||||
|
break;
|
||||||
|
case 'resend_email':
|
||||||
|
handleResendVerification(user);
|
||||||
|
break;
|
||||||
|
case 'activate_payment':
|
||||||
|
handleActivatePayment(user);
|
||||||
|
break;
|
||||||
|
case 'reactivate':
|
||||||
|
handleReactivateUser(user);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -385,9 +422,8 @@ const AdminValidations = () => {
|
|||||||
className="cursor-pointer hover:bg-[var(--neutral-800)]/20"
|
className="cursor-pointer hover:bg-[var(--neutral-800)]/20"
|
||||||
onClick={() => handleSort('first_name')}
|
onClick={() => handleSort('first_name')}
|
||||||
>
|
>
|
||||||
Name {renderSortIcon('first_name')}
|
Member {renderSortIcon('first_name')}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead>Email</TableHead>
|
|
||||||
<TableHead>Phone</TableHead>
|
<TableHead>Phone</TableHead>
|
||||||
<TableHead
|
<TableHead
|
||||||
className="cursor-pointer hover:bg-[var(--neutral-800)]/20"
|
className="cursor-pointer hover:bg-[var(--neutral-800)]/20"
|
||||||
@@ -415,11 +451,15 @@ const AdminValidations = () => {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{paginatedUsers.map((user) => (
|
{paginatedUsers.map((user) => (
|
||||||
<TableRow key={user.id}>
|
<TableRow key={user.id}>
|
||||||
<TableCell className="font-medium">
|
<TableCell className=" ">
|
||||||
|
<div className='font-bold'>
|
||||||
|
|
||||||
{user.first_name} {user.last_name}
|
{user.first_name} {user.last_name}
|
||||||
|
</div>
|
||||||
|
{user.email}
|
||||||
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{user.email}</TableCell>
|
<TableCell>{formatPhoneNumber(user.phone)}</TableCell>
|
||||||
<TableCell>{user.phone}</TableCell>
|
|
||||||
<TableCell><StatusBadge status={user.status} /></TableCell>
|
<TableCell><StatusBadge status={user.status} /></TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{new Date(user.created_at).toLocaleDateString()}
|
{new Date(user.created_at).toLocaleDateString()}
|
||||||
@@ -433,98 +473,76 @@ const AdminValidations = () => {
|
|||||||
{user.referred_by_member_name || '-'}
|
{user.referred_by_member_name || '-'}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex gap-2">
|
<div className='flex justify-between'>
|
||||||
{user.status === 'rejected' ? (
|
|
||||||
<Button
|
<Select
|
||||||
onClick={() => handleReactivateUser(user)}
|
value=""
|
||||||
disabled={actionLoading === user.id}
|
onValueChange={(action) => handleActionSelect(user, action)}
|
||||||
size="sm"
|
disabled={actionLoading === user.id || resendLoading === user.id}
|
||||||
className="bg-[var(--green-light)] text-white hover:bg-[var(--green-mint)]"
|
|
||||||
>
|
>
|
||||||
{actionLoading === user.id ? 'Reactivating...' : 'Reactivate'}
|
<SelectTrigger className="w-[180px] h-9 border-[var(--neutral-800)]">
|
||||||
</Button>
|
<SelectValue placeholder={actionLoading === user.id || resendLoading === user.id ? 'Processing...' : 'Select Action'} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{user.status === 'rejected' ? (
|
||||||
|
<SelectItem value="reactivate">Reactivate</SelectItem>
|
||||||
) : user.status === 'pending_email' ? (
|
) : user.status === 'pending_email' ? (
|
||||||
<>
|
<>
|
||||||
{hasPermission('users.approve') && (
|
{hasPermission('users.approve') && (
|
||||||
<Button
|
<SelectItem value="bypass_validate">Bypass & Validate</SelectItem>
|
||||||
onClick={() => handleBypassAndValidateRequest(user)}
|
|
||||||
disabled={actionLoading === user.id}
|
|
||||||
size="sm"
|
|
||||||
className="bg-secondary text-[var(--purple-ink)] hover:bg-secondary/80"
|
|
||||||
>
|
|
||||||
{actionLoading === user.id ? 'Validating...' : 'Bypass & Validate'}
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
{hasPermission('users.approve') && (
|
{hasPermission('users.approve') && (
|
||||||
<Button
|
<SelectItem value="resend_email">Resend Email</SelectItem>
|
||||||
disabled={resendLoading === user.id}
|
|
||||||
onClick={() => handleResendVerification(user)}
|
|
||||||
size="sm"
|
|
||||||
className=" bg-secondary text-[var(--purple-ink)] hover:bg-secondary/80"
|
|
||||||
>
|
|
||||||
{resendLoading === user.id ? 'Sending...' : 'Resend email'}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{hasPermission('users.approve') && (
|
|
||||||
<Button
|
|
||||||
onClick={() => handleRejectUser(user)}
|
|
||||||
disabled={actionLoading === user.id}
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
className="border-2 border-red-500 text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10"
|
|
||||||
>
|
|
||||||
Reject
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
|
{/* {hasPermission('users.approve') && (
|
||||||
|
<SelectItem value="reject">Reject</SelectItem>
|
||||||
|
)} */}
|
||||||
</>
|
</>
|
||||||
) : user.status === 'payment_pending' ? (
|
) : user.status === 'payment_pending' ? (
|
||||||
<>
|
<>
|
||||||
{hasPermission('subscriptions.activate') && (
|
{hasPermission('subscriptions.activate') && (
|
||||||
<Button
|
<SelectItem value="activate_payment">Activate Payment</SelectItem>
|
||||||
onClick={() => handleActivatePayment(user)}
|
|
||||||
size="sm"
|
|
||||||
className="btn-light-lavender"
|
|
||||||
>
|
|
||||||
Activate Payment
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{hasPermission('users.approve') && (
|
|
||||||
<Button
|
|
||||||
onClick={() => handleRejectUser(user)}
|
|
||||||
disabled={actionLoading === user.id}
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
className="border-2 border-red-500 text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10"
|
|
||||||
>
|
|
||||||
Reject
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
|
{/* {hasPermission('users.approve') && (
|
||||||
|
<SelectItem value="reject">Reject</SelectItem>
|
||||||
|
)} */}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{hasPermission('users.approve') && (
|
{hasPermission('users.approve') && (
|
||||||
|
<SelectItem value="validate">Validate</SelectItem>
|
||||||
|
)}
|
||||||
|
{/* {hasPermission('users.approve') && (
|
||||||
|
<SelectItem value="reject">Reject</SelectItem>
|
||||||
|
)} */}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{/* view registration */}
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleValidateRequest(user)}
|
onClick={() => handleRegistrationDialog(user)}
|
||||||
disabled={actionLoading === user.id}
|
disabled={actionLoading === user.id}
|
||||||
size="sm"
|
size="sm"
|
||||||
className="bg-[var(--green-light)] text-white hover:bg-[var(--green-mint)]"
|
variant="outline"
|
||||||
|
className="border-2 mr-2 border-primary text-primary hover:bg-red-50 dark:hover:bg-red-500/10"
|
||||||
>
|
>
|
||||||
{actionLoading === user.id ? 'Validating...' : 'Validate'}
|
<FileText className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
|
||||||
|
{/* reject */}
|
||||||
{hasPermission('users.approve') && (
|
{hasPermission('users.approve') && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleRejectUser(user)}
|
onClick={() => handleRejectUser(user)}
|
||||||
disabled={actionLoading === user.id}
|
disabled={actionLoading === user.id}
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="border-2 border-red-500 text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10"
|
className="border-2 mr-2 border-red-500 text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10"
|
||||||
>
|
>
|
||||||
Reject
|
X
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -649,6 +667,13 @@ const AdminValidations = () => {
|
|||||||
user={userToReject}
|
user={userToReject}
|
||||||
loading={actionLoading !== null}
|
loading={actionLoading !== null}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* View Registration Dialog */}
|
||||||
|
<ViewRegistrationDialog
|
||||||
|
open={viewRegistrationDialogOpen}
|
||||||
|
onOpenChange={setViewRegistrationDialogOpen}
|
||||||
|
user={selectedUserForView}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user