import { useState, useEffect, useCallback } from 'react'; import { adminData } from '@/lib/adminApi'; import { auditedAdminData } from '@/hooks/useAuditedAdminData'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; import { Skeleton } from '@/components/ui/skeleton'; import { PaginationControls } from '@/components/ui/pagination'; import { Switch } from '@/components/ui/switch'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { Search, Ticket, Trash2, Plus, Copy, Users } from 'lucide-react'; import { format } from 'date-fns'; import { toast } from 'sonner'; interface RedemptionCode { id: string; code: string; description: string | null; max_uses: number; current_uses: number; expires_at: string | null; is_active: boolean; created_by: string | null; created_at: string; updated_at: string; } interface CodeRedemption { id: string; user_id: string; redeemed_at: string; profile?: { name: string | null; email: string | null; }; } function generateCode(length: number = 8): string { const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // Removed confusing chars like 0, O, I, 1 let result = ''; for (let i = 0; i < length; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)); } return result; } export function RedemptionCodesPage() { const [codes, setCodes] = useState([]); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(''); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(25); const [totalCount, setTotalCount] = useState(0); // Add Code State const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ code: '', description: '', max_uses: 1, expires_at: '', }); // View Redemptions State const [viewingCode, setViewingCode] = useState(null); const [redemptions, setRedemptions] = useState([]); const [loadingRedemptions, setLoadingRedemptions] = useState(false); const [redemptionsPage, setRedemptionsPage] = useState(1); const [redemptionsPageSize, setRedemptionsPageSize] = useState(10); const [redemptionsTotal, setRedemptionsTotal] = useState(0); const fetchCodes = useCallback(async () => { setLoading(true); try { const from = (page - 1) * pageSize; const to = from + pageSize - 1; const trimmedSearch = search.trim(); let query = adminData .from('redemption_codes') .select('*', { count: 'exact' }) .order('created_at', { ascending: false }); if (trimmedSearch) { query = query.or(`code.ilike.%${trimmedSearch}%,description.ilike.%${trimmedSearch}%`); } const { data, error, count } = await query.range(from, to); if (error) throw error; setCodes(data || []); setTotalCount(count || 0); } catch (error: any) { console.error('Failed to load codes:', error); toast.error("Failed to load redemption codes"); } finally { setLoading(false); } }, [page, pageSize, search]); const fetchRedemptions = useCallback(async (codeId: string) => { setLoadingRedemptions(true); try { const from = (redemptionsPage - 1) * redemptionsPageSize; const to = from + redemptionsPageSize - 1; const { data: redemptionData, error: redemptionError, count } = await adminData .from('code_redemptions') .select('id, user_id, redeemed_at', { count: 'exact' }) .eq('code_id', codeId) .order('redeemed_at', { ascending: false }) .range(from, to); if (redemptionError) throw redemptionError; setRedemptionsTotal(count || 0); if (redemptionData && redemptionData.length > 0) { const userIds = redemptionData.map(r => r.user_id); const { data: profileData } = await adminData .from('profiles') .select('id, name, email') .in('id', userIds); const joined = redemptionData.map(r => ({ ...r, profile: profileData?.find(p => p.id === r.user_id) })); setRedemptions(joined); } else { setRedemptions([]); } } catch (error: any) { console.error('Failed to load redemptions:', error); toast.error("Failed to load redemptions"); } finally { setLoadingRedemptions(false); } }, [redemptionsPage, redemptionsPageSize]); useEffect(() => { fetchCodes(); }, [fetchCodes]); useEffect(() => { setPage(1); }, [search]); useEffect(() => { const totalPages = Math.max(1, Math.ceil(totalCount / pageSize)); if (page > totalPages) { setPage(totalPages); } }, [page, pageSize, totalCount]); useEffect(() => { if (viewingCode) { fetchRedemptions(viewingCode.id); } }, [viewingCode, fetchRedemptions]); useEffect(() => { const totalPages = Math.max(1, Math.ceil(redemptionsTotal / redemptionsPageSize)); if (redemptionsPage > totalPages) { setRedemptionsPage(totalPages); } }, [redemptionsPage, redemptionsPageSize, redemptionsTotal]); const handleAddCode = async () => { if (!formData.code.trim()) { toast.error("Please enter a code"); return; } setIsSubmitting(true); try { const { error } = await auditedAdminData.insert('redemption_codes', { code: formData.code.toUpperCase().trim(), description: formData.description.trim() || null, max_uses: formData.max_uses, current_uses: 0, expires_at: formData.expires_at || null, is_active: true, }); if (error) throw error; toast.success("Redemption code created"); setIsAddDialogOpen(false); resetForm(); fetchCodes(); } catch (error: any) { console.error('Failed to create code:', error); if (error.message?.includes('duplicate') || error.code === '23505') { toast.error("This code already exists"); } else { toast.error(error.message || "Failed to create code"); } } finally { setIsSubmitting(false); } }; const handleToggleActive = async (code: RedemptionCode) => { try { const { error } = await auditedAdminData.update('redemption_codes', code.id, { is_active: !code.is_active, }); if (error) throw error; toast.success(code.is_active ? "Code deactivated" : "Code activated"); fetchCodes(); } catch (error: any) { toast.error("Failed to update code"); } }; const handleDeleteCode = async (code: RedemptionCode) => { if (!confirm(`Are you sure you want to delete the code "${code.code}"? This cannot be undone.`)) { return; } try { const { error } = await auditedAdminData.delete('redemption_codes', code.id); if (error) throw error; toast.success("Code deleted"); fetchCodes(); } catch (error: any) { toast.error("Failed to delete code"); } }; const copyToClipboard = (text: string) => { navigator.clipboard.writeText(text); toast.success("Copied to clipboard"); }; const resetForm = () => { setFormData({ code: '', description: '', max_uses: 1, expires_at: '', }); }; const handleOpenAddDialog = () => { resetForm(); setFormData(prev => ({ ...prev, code: generateCode() })); setIsAddDialogOpen(true); }; const handleViewRedemptions = (code: RedemptionCode) => { setViewingCode(code); setRedemptionsPage(1); }; const filteredCodes = codes; const getStatusBadge = (code: RedemptionCode) => { if (!code.is_active) { return Inactive; } if (code.expires_at && new Date(code.expires_at) < new Date()) { return Expired; } if (code.current_uses >= code.max_uses) { return Exhausted; } return Active; }; return (

Redemption Codes

Generate and manage codes that grant premium access

setSearch(e.target.value)} className="pl-9" />
Generate Redemption Code Create a new code that users can redeem for premium access.
setFormData(prev => ({ ...prev, code: e.target.value.toUpperCase() }))} placeholder="e.g., PREMIUM2024" className="font-mono" />
setFormData(prev => ({ ...prev, description: e.target.value }))} placeholder="e.g., Early adopter reward" />
setFormData(prev => ({ ...prev, max_uses: parseInt(e.target.value) || 1 }))} />

How many times this code can be redeemed

setFormData(prev => ({ ...prev, expires_at: e.target.value }))} />
Code Description Usage Expires Status Created Actions {loading ? (
) : filteredCodes.length === 0 ? ( {search ? 'No codes match your search.' : 'No redemption codes yet. Generate your first one!'} ) : ( filteredCodes.map((code) => { const statusBadge = getStatusBadge(code); const expiresLabel = code.expires_at ? format(new Date(code.expires_at), 'MMM d, yyyy HH:mm') : 'Never'; return (
{code.code}
{statusBadge} {code.current_uses} / {code.max_uses} uses Expires {expiresLabel} {code.description && {code.description}}
handleToggleActive(code)} title={code.is_active ? "Deactivate" : "Activate"} /> {code.is_active ? 'Active' : 'Inactive'}
{code.description || '—'} {code.current_uses} / {code.max_uses} {expiresLabel} {statusBadge} {format(new Date(code.created_at), 'MMM d, yyyy')}
handleToggleActive(code)} title={code.is_active ? "Deactivate" : "Activate"} />
); }) )}
{filteredCodes.length > 0 && ( { setPage(1); setPageSize(size); }} /> )} {/* View Redemptions Dialog */} !open && setViewingCode(null)}> Redemptions for {viewingCode?.code} {viewingCode?.current_uses || 0} of {viewingCode?.max_uses} uses
{loadingRedemptions ? (
) : redemptions.length === 0 ? (

No one has redeemed this code yet.

) : ( User Redeemed At {redemptions.map((r) => (
{r.profile?.name || 'Unknown'}
{r.profile?.email}
{format(new Date(r.redeemed_at), 'MMM d, yyyy HH:mm')}
))}
)}
{redemptionsTotal > 0 && ( { setRedemptionsPage(1); setRedemptionsPageSize(size); }} /> )}
); }