import { useState, useEffect, useCallback } from 'react'; import { Link } from 'react-router-dom'; import { supabase } from '@/config'; 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 { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { useRealtimeSubscription } from '@/hooks/useRealtimeSubscription'; import { RealtimeStatusIndicator } from '@/components/RealtimeStatusIndicator'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { Search, Crown, Users, ChevronRight, HardDrive, MailWarning } from 'lucide-react'; import { format } from 'date-fns'; interface Profile { id: string; name: string | null; email: string | null; avatar_url: string | null; is_premium: boolean | null; couple_id: string | null; created_at: string | null; last_sign_in_at: string | null; email_confirmed_at: string | null; storage_bytes?: number; partner_is_premium?: boolean; } function formatBytes(bytes: number): string { if (bytes === 0) return '—'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; } export function UsersPage() { const [profiles, setProfiles] = useState([]); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(''); const [filteredProfiles, setFilteredProfiles] = useState([]); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(25); const fetchProfiles = useCallback(async () => { setLoading(true); try { // Fetch profiles with auth info (includes last_sign_in_at) const { data: profilesDataRaw, error } = await supabase .rpc('get_profiles_with_auth_info'); if (error) throw error; const profilesData = (profilesDataRaw || []) as Profile[]; // Fetch storage usage per user from storage.objects const { data: storageData } = await supabase .rpc('get_user_storage_usage'); // Create a map of user_id -> storage_bytes const storageMap: Record = {}; if (storageData) { storageData.forEach((row: { owner: string; total_bytes: number }) => { storageMap[row.owner] = row.total_bytes; }); } // Build a map of couple_id -> users with premium in that couple const couplePartners: Record = {}; profilesData.forEach(profile => { if (profile.couple_id) { if (!couplePartners[profile.couple_id]) { couplePartners[profile.couple_id] = []; } couplePartners[profile.couple_id].push({ id: profile.id, is_premium: profile.is_premium || false, }); } }); // Merge storage data and partner premium status with profiles const profilesWithExtras = profilesData.map(profile => { // Check if partner has premium let partnerIsPremium = false; if (profile.couple_id && couplePartners[profile.couple_id]) { const partner = couplePartners[profile.couple_id].find(p => p.id !== profile.id); partnerIsPremium = partner?.is_premium || false; } return { ...profile, storage_bytes: storageMap[profile.id] || 0, partner_is_premium: partnerIsPremium, }; }); setProfiles(profilesWithExtras); setFilteredProfiles(profilesWithExtras); } catch (error) { console.error('Failed to load profiles:', error); } finally { setLoading(false); } }, []); // Real-time subscription for profiles table const { status: realtimeStatus } = useRealtimeSubscription({ table: 'profiles', onInsert: useCallback(() => { // Refetch to get storage and partner data fetchProfiles(); }, [fetchProfiles]), onUpdate: useCallback(() => { // Refetch to update partner premium status fetchProfiles(); }, [fetchProfiles]), onDelete: useCallback((deleted: Profile) => { setProfiles(prev => prev.filter(p => p.id !== deleted.id)); setFilteredProfiles(prev => prev.filter(p => p.id !== deleted.id)); }, []), insertToast: { enabled: true, message: (payload) => `New user registered: ${payload.name || payload.email || 'Unknown'}`, type: 'success', }, debounceMs: 1000, // Debounce to avoid excessive refetches }); useEffect(() => { fetchProfiles(); }, [fetchProfiles]); useEffect(() => { setPage(1); if (!search.trim()) { setFilteredProfiles(profiles); return; } const query = search.toLowerCase(); setFilteredProfiles( profiles.filter( p => p.name?.toLowerCase().includes(query) || p.email?.toLowerCase().includes(query) || p.id.toLowerCase().includes(query) ) ); }, [search, profiles]); const totalCount = filteredProfiles.length; const paginatedProfiles = filteredProfiles.slice( (page - 1) * pageSize, page * pageSize ); useEffect(() => { const totalPages = Math.max(1, Math.ceil(totalCount / pageSize)); if (page > totalPages) { setPage(totalPages); } }, [page, pageSize, totalCount]); if (loading) { return (
); } return (
{/* Header */}

Users

{profiles.length} registered user{profiles.length !== 1 ? 's' : ''}

setSearch(e.target.value)} className="pl-9" />
{/* Users Table */}
User Email Status Couple Storage Joined Last Active {filteredProfiles.length === 0 ? ( {search ? 'No users found matching your search.' : 'No users yet.'} ) : ( paginatedProfiles.map((profile) => ( {profile.name?.charAt(0).toUpperCase() || 'U'} {profile.name || 'Unnamed User'}
{profile.email || '—'} {profile.email && !profile.email_confirmed_at && ( )}
{profile.is_premium ? ( Premium ) : profile.partner_is_premium ? ( Partner ) : ( Free )} {profile.couple_id ? ( Paired ) : ( — )} {profile.storage_bytes && profile.storage_bytes > 0 ? (
{formatBytes(profile.storage_bytes)}
) : ( — )}
{profile.created_at ? format(new Date(profile.created_at), 'MMM d, yyyy') : '—'} {profile.last_sign_in_at ? format(new Date(profile.last_sign_in_at), 'MMM d, yyyy h:mm a') : 'Never'}
)) )}
{filteredProfiles.length > 0 && ( { setPage(1); setPageSize(size); }} /> )}
); }