import React, { useState, useMemo, useEffect } from 'react'; import { StyleSheet, ScrollView, TouchableOpacity, Image, FlatList, Platform, useWindowDimensions, RefreshControl, TextInput, KeyboardAvoidingView } from 'react-native'; import { View, Text, useTheme } from '@bettoredge/styles'; import { Ionicons } from '@expo/vector-icons'; import type { CalcuttaParticipantProps } from '@bettoredge/types'; import { useCalcuttaCompetition } from '../hooks/useCalcuttaCompetition'; import { useCalcuttaPlayers } from '../hooks/useCalcuttaPlayers'; import { useCalcuttaItemImages } from '../hooks/useCalcuttaItemImages'; import { useCalcuttaResults } from '../hooks/useCalcuttaResults'; import { useCalcuttaSocket } from '../hooks/useCalcuttaSocket'; import { formatCurrency, getStatusLabel, resolveItemImageUrl } from '../helpers/formatting'; export interface CalcuttaResultsProps { calcutta_competition_id: string; player_id?: string; access_token?: string; device_id?: string; player_username?: string; player_profile_pic?: string; onManage?: () => void; onShare?: () => void; onRefresh?: () => void | Promise; } export const CalcuttaResults: React.FC = ({ calcutta_competition_id, player_id, access_token, device_id, player_username, player_profile_pic, onManage, onShare, onRefresh, }) => { const { theme } = useTheme(); const { width: windowWidth } = useWindowDimensions(); const [containerWidth, setContainerWidth] = useState(0); const measuredWidth = containerWidth || windowWidth; const isDesktop = Platform.OS === 'web' && measuredWidth >= 700; const [refreshing, setRefreshing] = useState(false); const { loading, competition, rounds, items, participants, payout_rules, item_results, refresh, } = useCalcuttaCompetition(calcutta_competition_id); const participantIds = useMemo(() => participants.map(p => p.player_id), [participants]); const { players: enrichedPlayers } = useCalcuttaPlayers(participantIds); const { images: itemImages } = useCalcuttaItemImages(items); const { roundSummaries, itemPerformance } = useCalcuttaResults(rounds, items, item_results, payout_rules); const { presence, socketState } = useCalcuttaSocket( calcutta_competition_id, access_token, device_id, { username: player_username, profile_pic: player_profile_pic }, ); const handleRefresh = async () => { setRefreshing(true); try { await Promise.all([refresh(), onRefresh?.()]); } catch {} finally { setRefreshing(false); } }; // Derived data const isAdmin = player_id != null && competition?.admin_id == player_id; const myItems = useMemo(() => items.filter(i => i.winning_player_id == player_id).sort((a, b) => Number(b.winning_bid || 0) - Number(a.winning_bid || 0)), [items, player_id] ); const totalPot = useMemo(() => items.filter(i => i.status === 'sold').reduce((sum, i) => sum + Number(i.winning_bid || 0), 0) || Number(competition?.total_pot) || 0, [items, competition?.total_pot] ); const unclaimedPot = Number(competition?.unclaimed_pot) || 0; const myParticipant = useMemo(() => participants.find(p => p.player_id == player_id), [participants, player_id]); const myTotalSpent = Number(myParticipant?.total_spent) || 0; const myTotalEarned = Number(myParticipant?.total_winnings) || 0; const statusLabel = competition?.status === 'closed' ? 'Completed' : competition?.status === 'inprogress' ? 'In Progress' : 'Auction Closed'; const statusColor = competition?.status === 'closed' ? '#6B7280' : competition?.status === 'inprogress' ? '#8B5CF6' : '#F59E0B'; // Sorted leaderboard const leaderboard = useMemo(() => [...participants].sort((a, b) => { if (a.place && b.place) return a.place - b.place; if (a.place) return -1; if (b.place) return 1; return Number(b.total_winnings) - Number(a.total_winnings); }), [participants] ); // Mobile tab type BottomTab = 'rounds' | 'leaderboard' | 'items'; const [mobileTab, setMobileTab] = useState('rounds'); // Expandable rounds const [expandedRound, setExpandedRound] = useState(null); // Leaderboard search, pagination const [leaderSearch, setLeaderSearch] = useState(''); const LEADERS_PER_PAGE = 20; const [leaderPage, setLeaderPage] = useState(0); const filteredLeaderboard = useMemo(() => { if (!leaderSearch.trim()) return leaderboard; const q = leaderSearch.toLowerCase(); return leaderboard.filter(p => { const profile = enrichedPlayers[p.player_id]; const username = (profile?.username || profile?.show_name || '').toLowerCase(); if (username.includes(q)) return true; if (p.player_id == player_id && 'you'.includes(q)) return true; return false; }); }, [leaderboard, leaderSearch, enrichedPlayers, player_id]); const pagedLeaderboard = useMemo(() => { const start = leaderPage * LEADERS_PER_PAGE; return filteredLeaderboard.slice(start, start + LEADERS_PER_PAGE); }, [filteredLeaderboard, leaderPage]); const totalLeaderPages = Math.ceil(filteredLeaderboard.length / LEADERS_PER_PAGE); // Item search, sort, pagination const [itemSearch, setItemSearch] = useState(''); type ItemSort = 'name' | 'paid' | 'bid'; const [itemSort, setItemSort] = useState('paid'); const ITEMS_PER_PAGE = 20; const [itemPage, setItemPage] = useState(0); const itemEarningsMap = useMemo(() => { const map: Record = {}; for (const ip of itemPerformance) { map[ip.item.calcutta_auction_item_id] = ip.total_earned; } return map; }, [itemPerformance]); const filteredItems = useMemo(() => { let result = items; if (itemSearch.trim()) { const q = itemSearch.toLowerCase(); result = result.filter(i => { if (i.item_name.toLowerCase().includes(q)) return true; if (i.winning_player_id) { const owner = enrichedPlayers[i.winning_player_id]; if ((owner?.username || '').toLowerCase().includes(q)) return true; } return false; }); } return [...result].sort((a, b) => { switch (itemSort) { case 'paid': return (itemEarningsMap[b.calcutta_auction_item_id] || 0) - (itemEarningsMap[a.calcutta_auction_item_id] || 0); case 'bid': return Number(b.winning_bid || 0) - Number(a.winning_bid || 0); case 'name': default: return a.item_name.localeCompare(b.item_name); } }); }, [items, itemSearch, enrichedPlayers, itemSort, itemEarningsMap]); const pagedItems = useMemo(() => { const start = itemPage * ITEMS_PER_PAGE; return filteredItems.slice(start, start + ITEMS_PER_PAGE); }, [filteredItems, itemPage]); const totalItemPages = Math.ceil(filteredItems.length / ITEMS_PER_PAGE); if (loading && !competition) { return ( Loading... ); } if (!competition) { return ( Competition not found ); } const marketType = competition.market_type; const isSweepstakes = competition.auction_type === 'sweepstakes'; // ═══ RENDER HELPERS ═══ const renderHeader = () => ( {statusLabel} {isSweepstakes ? 'Sweepstakes' : competition.auction_type === 'live' ? 'Live Auction' : 'Sealed Bid'} {isAdmin && onManage && ( Manage )} {competition.competition_name} {competition.competition_description ? ( {competition.competition_description} ) : null} ); const renderMyItems = () => { if (myItems.length === 0) return null; return ( Your Items ({myItems.length}) {myItems.map(mi => { const imgUrl = resolveItemImageUrl(mi.item_image) || itemImages[mi.item_id]?.url; const isEliminated = mi.status === 'eliminated'; return ( {imgUrl ? ( ) : ( )} {mi.item_name} {mi.seed != null && #{mi.seed}} {Number(mi.winning_bid) > 0 && !isSweepstakes && ( {formatCurrency(mi.winning_bid, marketType)} )} {isEliminated && ( Eliminated )} ); })} ); }; const renderPotKPIs = () => { const kpiStyle = isDesktop ? [styles.kpiCard, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }] : [styles.kpiCard, { flex: 0, backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle, minWidth: '31%' as any, flexGrow: 1 }]; return ( {!isSweepstakes && ( Pot {formatCurrency(totalPot, marketType)} )} {!isSweepstakes && unclaimedPot > 0 && ( Unclaimed {formatCurrency(unclaimedPot, marketType)} )} {!isSweepstakes && myTotalSpent > 0 && ( You Spent {formatCurrency(myTotalSpent, marketType)} )} {myTotalEarned > 0 && ( You Earned {formatCurrency(myTotalEarned, marketType)} )} Players {participants.length} ); }; const renderRounds = () => { if (roundSummaries.length === 0) return null; const sortedRounds = [...roundSummaries].sort((a, b) => a.round.round_number - b.round.round_number); // Compute running unclaimed balance per round // Each closed round: pool = (pct * pot) + rolledIn, paid out = total_payout, rolled out = pool - paid const roundUnclaimed: { rolledIn: number; pool: number; rolledOut: number }[] = []; let runningUnclaimed = 0; for (const rs of sortedRounds) { const basePool = rs.payout_rule ? (rs.payout_rule.payout_pct / 100) * totalPot : 0; const pool = basePool + runningUnclaimed; const isClosed = rs.round.status === 'closed'; const rolledOut = isClosed ? Math.max(pool - rs.total_payout, 0) : 0; roundUnclaimed.push({ rolledIn: runningUnclaimed, pool, rolledOut }); if (isClosed) { runningUnclaimed = rolledOut; } } return ( Rounds {sortedRounds.map((rs, idx) => { const isExpanded = expandedRound === rs.round.round_number; const isClosed = rs.round.status === 'closed'; const isFinalRound = idx === sortedRounds.length - 1; const { rolledIn, pool: roundPool, rolledOut } = roundUnclaimed[idx]; return ( setExpandedRound(isExpanded ? null : rs.round.round_number)} activeOpacity={0.7} > {rs.round.round_name} {rs.payout_rule && ( {rs.payout_rule.payout_pct}% )} {isClosed && rs.payout_rule && !isSweepstakes && rs.total_payout > 0 && ( {formatCurrency(rs.total_payout, marketType)} )} {isClosed && rs.payout_rule && !isSweepstakes && rs.total_payout < 0.01 && !isFinalRound && ( Unclaimed )} {isClosed && isFinalRound && !isSweepstakes && runningUnclaimed > 0.01 && ( Redistributed )} {!isExpanded && isClosed && ( {rs.advanced_items.length} advanced · {rs.eliminated_items.length} eliminated )} {!isExpanded && !isClosed && rs.payout_rule && !isSweepstakes && ( {formatCurrency(roundPool, marketType)} payout pool{rolledIn > 0.01 ? ` (incl. ${formatCurrency(rolledIn, marketType)} rolled in)` : ''} )} {isExpanded && ( {!isSweepstakes && rs.payout_rule && ( {rs.payout_rule.payout_pct}% of pot{rolledIn > 0.01 ? ` + ${formatCurrency(rolledIn, marketType)} rolled in` : ''} {formatCurrency(roundPool, marketType)} {isClosed && ( <> Paid to owners {formatCurrency(rs.total_payout, marketType)} {rolledOut > 0.01 && !isFinalRound && ( Rolled to unclaimed {formatCurrency(rolledOut, marketType)} )} {isFinalRound && runningUnclaimed > 0.01 && ( Redistributed to all players by spend {formatCurrency(runningUnclaimed, marketType)} )} )} )} {rs.advanced_items.length > 0 && ( <> ADVANCED ({rs.advanced_items.length}) {rs.advanced_items.map(item => { const isMe = item.winning_player_id == player_id; const result = item_results.find(r => r.calcutta_auction_item_id === item.calcutta_auction_item_id && r.round_number === rs.round.round_number); const payout = Number(result?.payout_earned || 0); const owner = item.winning_player_id ? enrichedPlayers[item.winning_player_id] : undefined; const ownerName = isMe ? 'You' : owner?.username || owner?.show_name || ''; return ( {item.item_name} {ownerName ? {ownerName} : null} {payout > 0 && {formatCurrency(payout, marketType)}} ); })} )} {rs.eliminated_items.length > 0 && ( <> ELIMINATED ({rs.eliminated_items.length}) {rs.eliminated_items.map(item => { const isMe = item.winning_player_id == player_id; const result = item_results.find(r => r.calcutta_auction_item_id === item.calcutta_auction_item_id && r.round_number === rs.round.round_number); const payout = Number(result?.payout_earned || 0); const owner = item.winning_player_id ? enrichedPlayers[item.winning_player_id] : undefined; const ownerName = isMe ? 'You' : owner?.username || owner?.show_name || ''; return ( {item.item_name} {ownerName ? {ownerName} : null} {payout > 0 && {formatCurrency(payout, marketType)}} ); })} )} {isClosed && rs.advanced_items.length === 0 && rs.eliminated_items.length === 0 && ( No items remaining — round resolved with empty field )} {isClosed && !isSweepstakes && rolledOut > 0.01 && !isFinalRound && ( {formatCurrency(rolledOut, marketType)} unclaimed — rolls forward to next round )} {isClosed && !isSweepstakes && isFinalRound && runningUnclaimed > 0.01 && (() => { const totalSpentAll = participants.reduce((s, p) => s + Number(p.total_spent), 0); const playerShares = totalSpentAll > 0 ? participants .filter(p => Number(p.total_spent) > 0) .map(p => { const pct = (Number(p.total_spent) / totalSpentAll) * 100; const share = (pct / 100) * runningUnclaimed; const profile = enrichedPlayers[p.player_id]; const isMe = p.player_id == player_id; const name = isMe ? 'You' : profile?.username || profile?.show_name || 'Player'; return { name, share, pct, isMe }; }) .sort((a, b) => b.share - a.share) : []; return ( {formatCurrency(runningUnclaimed, marketType)} redistributed by spend {playerShares.map((ps, i) => ( {ps.name} ({ps.pct.toFixed(1)}%) {formatCurrency(ps.share, marketType)} ))} ); })()} {rs.prize_description && ( {rs.prize_description} )} )} ); })} ); }; const renderLeaderboard = () => { const medals = ['🥇', '🥈', '🥉']; // Map original rank from the full leaderboard const rankMap = new Map(); leaderboard.forEach((p, i) => rankMap.set(p.calcutta_participant_id, i)); return ( Leaderboard ({participants.length}) { setLeaderSearch(v); setLeaderPage(0); }} placeholder="Search players..." placeholderTextColor={theme.colors.text.tertiary} style={{ color: theme.colors.text.primary, backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle, borderWidth: 1, borderRadius: 8, paddingHorizontal: 10, paddingVertical: 8, fontSize: 13, marginBottom: 8, }} /> {pagedLeaderboard.map(p => { const rank = rankMap.get(p.calcutta_participant_id) ?? 0; const profile = enrichedPlayers[p.player_id]; const username = profile?.username || profile?.show_name || `Player`; const profilePic = profile?.profile_pic; const isMe = p.player_id == player_id; return ( {rank < 3 ? medals[rank] : `${rank + 1}`} {profilePic ? ( ) : ( {username.charAt(0).toUpperCase()} )} {isMe ? 'You' : username} {p.items_owned} item{p.items_owned !== 1 ? 's' : ''} · {formatCurrency(p.total_spent, marketType)} spent{totalPot > 0 ? ` · ${((Number(p.total_spent) / totalPot) * 100).toFixed(1)}%` : ''} {Number(p.total_spent) > 0 && ( {formatCurrency(p.total_winnings, marketType)} {(() => { const roi = ((Number(p.total_winnings) - Number(p.total_spent)) / Number(p.total_spent)) * 100; return ( = 0 ? '#10B981' : theme.colors.status.error, fontSize: 10, lineHeight: 13 }}> {roi >= 0 ? '+' : ''}{roi.toFixed(1)}% ); })()} )} ); })} {totalLeaderPages > 1 && ( setLeaderPage(p => Math.max(0, p - 1))} disabled={leaderPage === 0} style={{ opacity: leaderPage === 0 ? 0.3 : 1 }} > {leaderPage + 1} / {totalLeaderPages} setLeaderPage(p => Math.min(totalLeaderPages - 1, p + 1))} disabled={leaderPage >= totalLeaderPages - 1} style={{ opacity: leaderPage >= totalLeaderPages - 1 ? 0.3 : 1 }} > )} ); }; const renderAllItems = () => { const sortOptions: { key: ItemSort; label: string }[] = [ { key: 'paid', label: 'Most Paid' }, { key: 'bid', label: 'Highest Bid' }, { key: 'name', label: 'Name' }, ]; return ( All Items ({items.length}) {/* Search */} { setItemSearch(v); setItemPage(0); }} placeholder="Search items or owners..." placeholderTextColor={theme.colors.text.tertiary} style={{ color: theme.colors.text.primary, backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle, borderWidth: 1, borderRadius: 8, paddingHorizontal: 10, paddingVertical: 8, fontSize: 13, marginBottom: 8, }} /> {/* Sort pills */} {sortOptions.map(opt => { const active = itemSort === opt.key; return ( { setItemSort(opt.key); setItemPage(0); }} style={{ paddingHorizontal: 10, paddingVertical: 4, borderRadius: 12, backgroundColor: active ? theme.colors.primary.default : theme.colors.surface.elevated, borderWidth: 1, borderColor: active ? theme.colors.primary.default : theme.colors.border.subtle, }} > {opt.label} ); })} {/* Items */} {pagedItems.map(item => { const imgUrl = resolveItemImageUrl(item.item_image) || itemImages[item.item_id]?.url; const owner = item.winning_player_id ? enrichedPlayers[item.winning_player_id] : undefined; const ownerName = owner?.username || owner?.show_name; const isMe = item.winning_player_id == player_id; const earned = itemEarningsMap[item.calcutta_auction_item_id] || 0; return ( {imgUrl ? ( ) : ( )} {item.item_name} {ownerName ? (isMe ? 'You' : ownerName) : 'Unowned'} {item.status === 'eliminated' ? ' · Eliminated' : item.status === 'sold' ? '' : ` · ${getStatusLabel(item.status)}`} {Number(item.winning_bid) > 0 && !isSweepstakes && ( {formatCurrency(item.winning_bid, marketType)} )} {earned > 0 && ( {formatCurrency(earned, marketType)} earned )} {isMe && YOU} ); })} {/* Pagination */} {totalItemPages > 1 && ( setItemPage(p => Math.max(0, p - 1))} disabled={itemPage === 0} style={{ opacity: itemPage === 0 ? 0.3 : 1 }} > {itemPage + 1} / {totalItemPages} setItemPage(p => Math.min(totalItemPages - 1, p + 1))} disabled={itemPage >= totalItemPages - 1} style={{ opacity: itemPage >= totalItemPages - 1 ? 0.3 : 1 }} > )} ); }; // ═══ RENDER ═══ return ( setContainerWidth(e.nativeEvent.layout.width)}> {isDesktop ? ( }> {renderHeader()} {renderMyItems()} {renderPotKPIs()} {/* Left: Rounds */} {renderRounds()} {/* Right: Leaderboard */} {renderLeaderboard()} {renderAllItems()} ) : ( }> {renderHeader()} {renderMyItems()} {renderPotKPIs()} {([ { key: 'rounds' as BottomTab, label: 'Rounds', icon: 'layers-outline' as const }, { key: 'leaderboard' as BottomTab, label: 'Leaderboard', icon: 'podium-outline' as const }, { key: 'items' as BottomTab, label: 'Items', icon: 'list-outline' as const }, ]).map(tab => { const active = mobileTab === tab.key; return ( setMobileTab(tab.key)} style={{ flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 6, height: 44, backgroundColor: active ? theme.colors.primary.default : 'transparent' }} activeOpacity={0.7} > {tab.label} ); })} {mobileTab === 'rounds' && renderRounds()} {mobileTab === 'leaderboard' && renderLeaderboard()} {mobileTab === 'items' && renderAllItems()} )} ); }; const styles = StyleSheet.create({ container: { flex: 1, }, sectionLabel: { textTransform: 'uppercase', letterSpacing: 1, fontSize: 10, lineHeight: 13, marginBottom: 8, marginTop: 16, }, kpiCard: { flex: 1, borderRadius: 10, padding: 10, alignItems: 'center', borderWidth: 1, }, roundCard: { borderRadius: 10, borderWidth: 1, padding: 12, marginBottom: 8, }, leaderRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 8, borderBottomWidth: 1, }, panel: { borderRadius: 12, borderWidth: 1, padding: 8, }, });