|
import React, { useState, useMemo } from 'react'; |
|
import { TrendingUp, Award, Grid3X3, DollarSign, Target, Filter } from 'lucide-react'; |
|
import { LotomaniaGame, LotomaniaResult } from '../types'; |
|
import { LotomaniaAlgorithm } from '../utils/lotomaniaAlgorithm'; |
|
|
|
interface DetailedGameAnalysisProps { |
|
allGames: LotomaniaGame[]; |
|
algorithm: LotomaniaAlgorithm; |
|
currentResult: LotomaniaResult; |
|
analyzeGameResult: (numbers: number[], result: LotomaniaResult) => any; |
|
} |
|
|
|
interface DetailedGameResult { |
|
game: LotomaniaGame; |
|
numbers: number[]; |
|
matchedNumbers: number[]; |
|
nonMatchedNumbers: number[]; |
|
drawnButNotPlayed: number[]; |
|
points: number; |
|
prizeValue: number; |
|
prizeDescription: string; |
|
netProfit: number; |
|
isWinning: boolean; |
|
} |
|
|
|
export const DetailedGameAnalysis: React.FC<DetailedGameAnalysisProps> = ({ |
|
allGames, |
|
algorithm, |
|
currentResult, |
|
analyzeGameResult |
|
}) => { |
|
const [filterType, setFilterType] = useState<'all' | 'vertical' | 'horizontal'>('all'); |
|
const [filterPoints, setFilterPoints] = useState<'all' | '15+' | '16+' | '17+' | '18+' | '19+' | '20'>('all'); |
|
const [currentPage, setCurrentPage] = useState(1); |
|
const [showGrid, setShowGrid] = useState(true); |
|
const GAMES_PER_PAGE = 5; |
|
|
|
|
|
const detailedResults = useMemo(() => { |
|
console.log('🔄 Analisando todos os jogos detalhadamente...'); |
|
|
|
const results: DetailedGameResult[] = allGames.map(game => { |
|
const gameNumbers = algorithm.getNumbersFromGame(game); |
|
const analysis = analyzeGameResult(gameNumbers, currentResult); |
|
|
|
|
|
const matchedNumbers = gameNumbers.filter(num => currentResult.numeros.includes(num)); |
|
const nonMatchedNumbers = gameNumbers.filter(num => !currentResult.numeros.includes(num)); |
|
const drawnButNotPlayed = currentResult.numeros.filter(num => !gameNumbers.includes(num)); |
|
|
|
|
|
let prizeValue = 0; |
|
let prizeDescription = 'Sem prêmio'; |
|
|
|
const premio = currentResult.premiacoes?.find(p => p.acertos === analysis.points); |
|
if (premio && analysis.points >= 15) { |
|
prizeValue = premio.valorPremio; |
|
prizeDescription = premio.descricao; |
|
} else if (analysis.points === 0) { |
|
const premioZero = currentResult.premiacoes?.find(p => p.acertos === 0); |
|
if (premioZero) { |
|
prizeValue = premioZero.valorPremio; |
|
prizeDescription = premioZero.descricao; |
|
} |
|
} |
|
|
|
const netProfit = prizeValue - 3.00; |
|
|
|
return { |
|
game, |
|
numbers: gameNumbers, |
|
matchedNumbers, |
|
nonMatchedNumbers, |
|
drawnButNotPlayed, |
|
points: analysis.points, |
|
prizeValue, |
|
prizeDescription, |
|
netProfit, |
|
isWinning: prizeValue > 0 |
|
}; |
|
}); |
|
|
|
console.log(`✅ Análise detalhada concluída: ${results.length} jogos processados`); |
|
return results; |
|
}, [allGames, algorithm, currentResult, analyzeGameResult]); |
|
|
|
|
|
const filteredResults = useMemo(() => { |
|
let filtered = detailedResults; |
|
|
|
|
|
if (filterType !== 'all') { |
|
filtered = filtered.filter(r => r.game.type === filterType); |
|
} |
|
|
|
|
|
if (filterPoints === '20') { |
|
filtered = filtered.filter(r => r.points === 20 || r.points === 0); |
|
} else if (filterPoints !== 'all') { |
|
const minPoints = parseInt(filterPoints.replace('+', '')); |
|
filtered = filtered.filter(r => r.points >= minPoints); |
|
} |
|
|
|
return filtered; |
|
}, [detailedResults, filterType, filterPoints]); |
|
|
|
|
|
const totalPages = Math.ceil(filteredResults.length / GAMES_PER_PAGE); |
|
const paginatedResults = filteredResults.slice( |
|
(currentPage - 1) * GAMES_PER_PAGE, |
|
currentPage * GAMES_PER_PAGE |
|
); |
|
|
|
|
|
const statistics = useMemo(() => { |
|
const totalGames = filteredResults.length; |
|
const winningGames = filteredResults.filter(r => r.isWinning).length; |
|
const totalCost = totalGames * 3.00; |
|
const totalPrizes = filteredResults.reduce((sum, r) => sum + r.prizeValue, 0); |
|
const netResult = totalPrizes - totalCost; |
|
|
|
|
|
const pointsDistribution = filteredResults.reduce((acc, r) => { |
|
acc[r.points] = (acc[r.points] || 0) + 1; |
|
return acc; |
|
}, {} as Record<number, number>); |
|
|
|
|
|
const prizeDistribution = filteredResults |
|
.filter(r => r.isWinning) |
|
.reduce((acc, r) => { |
|
if (!acc[r.points]) { |
|
acc[r.points] = { count: 0, totalValue: 0 }; |
|
} |
|
acc[r.points].count++; |
|
acc[r.points].totalValue += r.prizeValue; |
|
return acc; |
|
}, {} as Record<number, { count: number; totalValue: number }>); |
|
|
|
return { |
|
totalGames, |
|
winningGames, |
|
totalCost, |
|
totalPrizes, |
|
netResult, |
|
winRate: (winningGames / totalGames) * 100, |
|
pointsDistribution, |
|
prizeDistribution |
|
}; |
|
}, [filteredResults]); |
|
|
|
|
|
const renderGameGrid = (result: DetailedGameResult) => { |
|
if (!showGrid) return null; |
|
|
|
return ( |
|
<div className="bg-white p-4 rounded-lg border mb-4"> |
|
<h6 className="font-semibold mb-3 flex items-center gap-2"> |
|
<Grid3X3 className="w-4 h-4" /> |
|
Grid de Marcação - Jogo #{result.game.id} |
|
</h6> |
|
|
|
{/* Headers */} |
|
<div className="flex gap-1 mb-2"> |
|
<div className="w-8"></div> |
|
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(col => ( |
|
<div key={col} className="w-8 h-6 text-xs font-bold text-center text-gray-600"> |
|
C{col} |
|
</div> |
|
))} |
|
</div> |
|
|
|
{/* Grid 10x10 */} |
|
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(row => ( |
|
<div key={row} className="flex gap-1 mb-1"> |
|
<div className="w-8 h-8 text-xs font-bold text-center text-gray-600 flex items-center justify-center"> |
|
L{row} |
|
</div> |
|
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(col => { |
|
const number = (row - 1) * 10 + (col - 1); // 0-99 |
|
const isMarked = result.numbers.includes(number); |
|
const isHit = result.matchedNumbers.includes(number); |
|
const wasDrawn = currentResult.numeros.includes(number); |
|
|
|
let cellClass = ''; |
|
let icon = ''; |
|
|
|
if (isMarked && isHit) { |
|
cellClass = 'bg-green-600 text-white border-green-700'; // Marcado e acertou |
|
icon = '✓'; |
|
} else if (isMarked && !isHit) { |
|
cellClass = 'bg-blue-600 text-white border-blue-700'; // Marcado mas não acertou |
|
} else if (!isMarked && wasDrawn) { |
|
cellClass = 'bg-yellow-400 text-black border-yellow-500'; // Não marcado mas saiu |
|
icon = '★'; |
|
} else { |
|
cellClass = 'bg-gray-100 text-gray-600 border-gray-300'; // Normal |
|
} |
|
|
|
return ( |
|
<div |
|
key={`${row}-${col}`} |
|
className={`w-8 h-8 border text-xs font-bold flex flex-col items-center justify-center relative ${cellClass}`} |
|
title={`Número ${number === 0 ? '00' : number} ${isMarked ? '(Marcado)' : ''} ${wasDrawn ? '(Sorteado)' : ''}`} |
|
> |
|
<span className="text-[10px] leading-none"> |
|
{number === 0 ? '00' : number.toString().padStart(2, '0')} |
|
</span> |
|
{icon && ( |
|
<span className="text-[8px] leading-none font-black"> |
|
{icon} |
|
</span> |
|
)} |
|
</div> |
|
); |
|
})} |
|
</div> |
|
))} |
|
|
|
{/* Legenda */} |
|
<div className="flex flex-wrap gap-3 mt-3 text-xs"> |
|
<div className="flex items-center gap-1"> |
|
<div className="w-4 h-4 bg-green-600 rounded flex items-center justify-center text-white text-[8px]">✓</div> |
|
<span>Marcado + Acertou ({result.matchedNumbers.length})</span> |
|
</div> |
|
<div className="flex items-center gap-1"> |
|
<div className="w-4 h-4 bg-blue-600 rounded"></div> |
|
<span>Marcado + Não acertou ({result.numbers.length - result.matchedNumbers.length})</span> |
|
</div> |
|
<div className="flex items-center gap-1"> |
|
<div className="w-4 h-4 bg-yellow-400 rounded flex items-center justify-center text-black text-[8px]">★</div> |
|
<span>Não marcado + Saiu ({result.drawnButNotPlayed.length})</span> |
|
</div> |
|
<div className="flex items-center gap-1"> |
|
<div className="w-4 h-4 bg-gray-100 border border-gray-300 rounded"></div> |
|
<span>Normal</span> |
|
</div> |
|
</div> |
|
</div> |
|
); |
|
}; |
|
|
|
return ( |
|
<div className="space-y-6"> |
|
{/* Header */} |
|
<div className="bg-gradient-to-r from-purple-600 to-blue-600 text-white p-6 rounded-lg"> |
|
<h2 className="text-2xl font-bold flex items-center gap-2"> |
|
<Target className="w-6 h-6" /> |
|
Análise Detalhada Jogo por Jogo |
|
</h2> |
|
<p className="text-purple-100 mt-2"> |
|
Resultado individual de cada jogo contra o concurso {currentResult.concurso} |
|
</p> |
|
</div> |
|
|
|
{/* Estatísticas Resumidas */} |
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> |
|
<div className="bg-blue-50 p-4 rounded-lg border-l-4 border-blue-500"> |
|
<span className="text-sm text-blue-600">Total de Jogos</span> |
|
<p className="text-2xl font-bold text-blue-800">{statistics.totalGames}</p> |
|
<p className="text-sm text-blue-600"> |
|
{filterType === 'all' ? 'Todos os tipos' : filterType} |
|
</p> |
|
</div> |
|
|
|
<div className="bg-green-50 p-4 rounded-lg border-l-4 border-green-500"> |
|
<span className="text-sm text-green-600">Jogos Premiados</span> |
|
<p className="text-2xl font-bold text-green-800">{statistics.winningGames}</p> |
|
<p className="text-sm text-green-600"> |
|
{statistics.winRate.toFixed(1)}% de aproveitamento |
|
</p> |
|
</div> |
|
|
|
<div className="bg-yellow-50 p-4 rounded-lg border-l-4 border-yellow-500"> |
|
<span className="text-sm text-yellow-600">Total de Prêmios</span> |
|
<p className="text-2xl font-bold text-yellow-800"> |
|
R$ {statistics.totalPrizes.toLocaleString('pt-BR', { maximumFractionDigits: 0 })} |
|
</p> |
|
<p className="text-sm text-yellow-600"> |
|
Custo: R$ {statistics.totalCost.toLocaleString('pt-BR', { maximumFractionDigits: 0 })} |
|
</p> |
|
</div> |
|
|
|
<div className={`p-4 rounded-lg border-l-4 ${ |
|
statistics.netResult >= 0 ? 'border-green-500 bg-green-50' : 'border-red-500 bg-red-50' |
|
}`}> |
|
<span className={`text-sm ${statistics.netResult >= 0 ? 'text-green-600' : 'text-red-600'}`}> |
|
Resultado Líquido |
|
</span> |
|
<p className={`text-2xl font-bold ${statistics.netResult >= 0 ? 'text-green-800' : 'text-red-800'}`}> |
|
{statistics.netResult >= 0 ? '+' : ''}R$ {statistics.netResult.toLocaleString('pt-BR', { maximumFractionDigits: 0 })} |
|
</p> |
|
<p className={`text-sm ${statistics.netResult >= 0 ? 'text-green-600' : 'text-red-600'}`}> |
|
{statistics.netResult >= 0 ? 'Lucro' : 'Prejuízo'} |
|
</p> |
|
</div> |
|
</div> |
|
|
|
{/* Filtros e Controles */} |
|
<div className="bg-white p-4 rounded-lg shadow border"> |
|
<div className="flex flex-wrap gap-4 items-center"> |
|
<div className="flex items-center gap-2"> |
|
<Filter className="w-4 h-4 text-gray-500" /> |
|
<span className="text-sm font-medium">Filtros:</span> |
|
</div> |
|
|
|
<select |
|
value={filterType} |
|
onChange={(e) => setFilterType(e.target.value as any)} |
|
className="px-3 py-1 border border-gray-300 rounded-md text-sm" |
|
> |
|
<option value="all">Todos os Tipos</option> |
|
<option value="vertical">Apenas Verticais</option> |
|
<option value="horizontal">Apenas Horizontais</option> |
|
</select> |
|
|
|
<select |
|
value={filterPoints} |
|
onChange={(e) => setFilterPoints(e.target.value as any)} |
|
className="px-3 py-1 border border-gray-300 rounded-md text-sm" |
|
> |
|
<option value="all">Todas as Pontuações</option> |
|
<option value="15+">15+ Pontos (Premiados)</option> |
|
<option value="16+">16+ Pontos</option> |
|
<option value="17+">17+ Pontos</option> |
|
<option value="18+">18+ Pontos</option> |
|
<option value="19+">19+ Pontos</option> |
|
<option value="20">20 Pontos (Máximo)</option> |
|
</select> |
|
|
|
<label className="flex items-center gap-2 text-sm"> |
|
<input |
|
type="checkbox" |
|
checked={showGrid} |
|
onChange={(e) => setShowGrid(e.target.checked)} |
|
className="rounded" |
|
/> |
|
Mostrar Grid Visual |
|
</label> |
|
|
|
<div className="text-sm text-gray-600"> |
|
{filteredResults.length} jogo(s) encontrado(s) |
|
</div> |
|
</div> |
|
</div> |
|
|
|
{/* Lista Detalhada de Jogos */} |
|
<div className="space-y-6"> |
|
{paginatedResults.map((result, index) => ( |
|
<div |
|
key={result.game.id} |
|
className={`p-6 border-2 rounded-xl shadow-lg ${ |
|
result.isWinning ? 'border-green-400 bg-green-50' : 'border-gray-300 bg-white' |
|
}`} |
|
> |
|
{/* Header do Jogo */} |
|
<div className="flex justify-between items-center mb-4"> |
|
<div className="flex items-center gap-4"> |
|
<div className="bg-blue-600 text-white px-4 py-2 rounded-lg"> |
|
<span className="font-bold text-xl">Jogo #{result.game.id}</span> |
|
</div> |
|
<div className="text-sm text-gray-600"> |
|
<div>📍 Fase {result.game.phase}, Ciclo {result.game.cycle}</div> |
|
<div>🎯 {result.game.type === 'vertical' ? 'Vertical (Colunas)' : 'Horizontal (Linhas)'}</div> |
|
{result.game.type === 'vertical' ? ( |
|
<div>🔹 Colunas: {result.game.markedColumns.join(', ')}</div> |
|
) : ( |
|
<div>🔸 Linhas: {result.game.markedRows?.join(', ')}</div> |
|
)} |
|
</div> |
|
</div> |
|
<div className="text-right"> |
|
<div className={`text-4xl font-bold mb-1 ${ |
|
result.isWinning ? 'text-green-600' : 'text-gray-600' |
|
}`}> |
|
{result.points} pts |
|
</div> |
|
<div className={`text-xl font-semibold ${ |
|
result.isWinning ? 'text-green-600' : 'text-red-600' |
|
}`}> |
|
{result.isWinning |
|
? `+R$ ${result.prizeValue.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}` |
|
: `-R$ 3,00` |
|
} |
|
</div> |
|
<div className={`text-lg font-medium ${ |
|
result.netProfit >= 0 ? 'text-green-600' : 'text-red-600' |
|
}`}> |
|
Líquido: {result.netProfit >= 0 ? '+' : ''}R$ {result.netProfit.toLocaleString('pt-BR', { minimumFractionDigits: 2 })} |
|
</div> |
|
<div className="text-sm text-gray-600 mt-1"> |
|
{result.isWinning ? `🏆 ${result.prizeDescription}` : '❌ Não premiado'} |
|
</div> |
|
</div> |
|
</div> |
|
|
|
{/* Grid Visual */} |
|
{renderGameGrid(result)} |
|
|
|
{/* Resumo de Números */} |
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> |
|
<div className="bg-white p-4 rounded-lg border"> |
|
<h6 className="font-semibold text-green-600 mb-2 flex items-center gap-2"> |
|
<Award className="w-4 h-4" /> |
|
Números Acertados ({result.matchedNumbers.length}) |
|
</h6> |
|
<div className="flex flex-wrap gap-1"> |
|
{result.matchedNumbers.length > 0 ? result.matchedNumbers.map(num => ( |
|
<span |
|
key={num} |
|
className="bg-green-600 text-white px-2 py-1 rounded text-sm font-bold" |
|
> |
|
{num === 0 ? '00' : num.toString().padStart(2, '0')} |
|
</span> |
|
)) : ( |
|
<span className="text-gray-500 text-sm italic">Nenhum acerto</span> |
|
)} |
|
</div> |
|
</div> |
|
|
|
<div className="bg-white p-4 rounded-lg border"> |
|
<h6 className="font-semibold text-yellow-600 mb-2"> |
|
⭐ Números Sorteados Não Jogados ({result.drawnButNotPlayed.length}) |
|
</h6> |
|
<div className="flex flex-wrap gap-1"> |
|
{result.drawnButNotPlayed.map(num => ( |
|
<span |
|
key={num} |
|
className="bg-yellow-400 text-black px-2 py-1 rounded text-sm font-bold" |
|
> |
|
{num === 0 ? '00' : num.toString().padStart(2, '0')} |
|
</span> |
|
))} |
|
</div> |
|
</div> |
|
|
|
<div className={`p-4 rounded-lg border ${ |
|
result.isWinning ? 'bg-green-100 border-green-300' : 'bg-red-100 border-red-300' |
|
}`}> |
|
<h6 className={`font-semibold mb-2 flex items-center gap-2 ${ |
|
result.isWinning ? 'text-green-600' : 'text-red-600' |
|
}`}> |
|
<DollarSign className="w-4 h-4" /> |
|
Resultado Financeiro |
|
</h6> |
|
<div className="space-y-1 text-sm"> |
|
<div className="flex justify-between"> |
|
<span>Custo do jogo:</span> |
|
<span className="text-red-600 font-semibold">-R$ 3,00</span> |
|
</div> |
|
<div className="flex justify-between"> |
|
<span>Prêmio obtido:</span> |
|
<span className={`font-semibold ${ |
|
result.isWinning ? 'text-green-600' : 'text-gray-500' |
|
}`}> |
|
+R$ {result.prizeValue.toLocaleString('pt-BR', { minimumFractionDigits: 2 })} |
|
</span> |
|
</div> |
|
<div className="border-t pt-1 flex justify-between"> |
|
<span className="font-semibold">Saldo:</span> |
|
<span className={`font-bold text-lg ${ |
|
result.netProfit >= 0 ? 'text-green-600' : 'text-red-600' |
|
}`}> |
|
{result.netProfit >= 0 ? '+' : ''}R$ {result.netProfit.toLocaleString('pt-BR', { minimumFractionDigits: 2 })} |
|
</span> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
))} |
|
</div> |
|
|
|
{/* Paginação */} |
|
{totalPages > 1 && ( |
|
<div className="flex justify-center items-center gap-2"> |
|
<button |
|
onClick={() => setCurrentPage(Math.max(1, currentPage - 1))} |
|
disabled={currentPage === 1} |
|
className="px-3 py-1 border rounded-md disabled:opacity-50" |
|
> |
|
Anterior |
|
</button> |
|
|
|
<div className="flex gap-1"> |
|
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => { |
|
const page = currentPage <= 3 ? i + 1 : currentPage - 2 + i; |
|
if (page > totalPages) return null; |
|
|
|
return ( |
|
<button |
|
key={page} |
|
onClick={() => setCurrentPage(page)} |
|
className={`px-3 py-1 border rounded-md ${ |
|
currentPage === page ? 'bg-blue-600 text-white' : 'hover:bg-gray-100' |
|
}`} |
|
> |
|
{page} |
|
</button> |
|
); |
|
})} |
|
</div> |
|
|
|
<button |
|
onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))} |
|
disabled={currentPage === totalPages} |
|
className="px-3 py-1 border rounded-md disabled:opacity-50" |
|
> |
|
Próximo |
|
</button> |
|
|
|
<span className="text-sm text-gray-600 ml-2"> |
|
Página {currentPage} de {totalPages} |
|
</span> |
|
</div> |
|
)} |
|
|
|
{/* Resumo de Distribuição por Pontos */} |
|
<div className="bg-white p-6 rounded-lg shadow border"> |
|
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2"> |
|
<TrendingUp className="w-5 h-5" /> |
|
Distribuição de Resultados |
|
</h3> |
|
|
|
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-8 gap-3"> |
|
{Object.entries(statistics.pointsDistribution) |
|
.sort(([a], [b]) => parseInt(b) - parseInt(a)) |
|
.map(([points, count]) => { |
|
const prizeInfo = statistics.prizeDistribution[parseInt(points)]; |
|
const isWinning = prizeInfo && prizeInfo.count > 0; |
|
|
|
return ( |
|
<div |
|
key={points} |
|
className={`p-3 rounded-lg text-center border ${ |
|
isWinning ? 'border-green-300 bg-green-50' : 'border-gray-200 bg-gray-50' |
|
}`} |
|
> |
|
<div className={`font-bold text-xl ${ |
|
isWinning ? 'text-green-600' : 'text-gray-600' |
|
}`}> |
|
{points} |
|
</div> |
|
<div className="text-xs text-gray-600 mb-1">pontos</div> |
|
<div className={`text-sm font-medium ${ |
|
isWinning ? 'text-green-700' : 'text-gray-700' |
|
}`}> |
|
{count} jogos |
|
</div> |
|
{isWinning && ( |
|
<div className="text-xs text-green-600 font-semibold mt-1"> |
|
R$ {prizeInfo.totalValue.toLocaleString('pt-BR', { maximumFractionDigits: 0 })} |
|
</div> |
|
)} |
|
</div> |
|
); |
|
})} |
|
</div> |
|
</div> |
|
</div> |
|
); |
|
}; |
|
|
|
export default DetailedGameAnalysis; |
|
|