|
import React, { useState, useMemo } from 'react'; |
|
import { Target, TrendingUp, Filter, Eye } from 'lucide-react'; |
|
import { LotomaniaGame, LotomaniaResult } from '../types'; |
|
import { LotomaniaAlgorithm } from '../utils/lotomaniaAlgorithm'; |
|
|
|
interface GameByGameResultsProps { |
|
allGames: LotomaniaGame[]; |
|
algorithm: LotomaniaAlgorithm; |
|
currentResult: LotomaniaResult; |
|
} |
|
|
|
interface GameResultDetailed { |
|
game: LotomaniaGame; |
|
numbers: number[]; |
|
matchedNumbers: number[]; |
|
nonMatchedNumbers: number[]; |
|
points: number; |
|
prizeValue: number; |
|
prizeDescription: string; |
|
netProfit: number; |
|
isWinning: boolean; |
|
} |
|
|
|
export const GameByGameResults: React.FC<GameByGameResultsProps> = ({ |
|
allGames, |
|
algorithm, |
|
currentResult |
|
}) => { |
|
const [filterType, setFilterType] = useState<'all' | 'vertical' | 'horizontal'>('all'); |
|
const [filterPoints, setFilterPoints] = useState<'all' | 'winning' | '15+' | '16' | '17+' | '18' | '19+' | '20' | '0'>('all'); |
|
const [showGrid, setShowGrid] = useState(true); |
|
const [currentPage, setCurrentPage] = useState(1); |
|
const GAMES_PER_PAGE = 10; |
|
|
|
|
|
const gameResults = useMemo(() => { |
|
return allGames.map(game => { |
|
const gameNumbers = algorithm.getNumbersFromGame(game); |
|
const matchedNumbers = gameNumbers.filter(num => currentResult.numeros.includes(num)); |
|
const nonMatchedNumbers = gameNumbers.filter(num => !currentResult.numeros.includes(num)); |
|
|
|
let points = matchedNumbers.length; |
|
let prizeValue = 0; |
|
let prizeDescription = 'Sem prêmio'; |
|
|
|
|
|
const premio = currentResult.premiacoes?.find(p => p.acertos === points); |
|
if (premio && points >= 15) { |
|
prizeValue = premio.valorPremio; |
|
prizeDescription = premio.descricao; |
|
} else if (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, |
|
points, |
|
prizeValue, |
|
prizeDescription, |
|
netProfit, |
|
isWinning: prizeValue > 0 |
|
}; |
|
}); |
|
}, [allGames, algorithm, currentResult]); |
|
|
|
|
|
const filteredResults = useMemo(() => { |
|
let filtered = gameResults; |
|
|
|
if (filterType !== 'all') { |
|
filtered = filtered.filter(r => r.game.type === filterType); |
|
} |
|
|
|
if (filterPoints === 'winning') { |
|
filtered = filtered.filter(r => r.isWinning); |
|
} else if (filterPoints === '20') { |
|
filtered = filtered.filter(r => r.points === 20); |
|
} else if (filterPoints === '18') { |
|
filtered = filtered.filter(r => r.points === 18); |
|
} else if (filterPoints === '16') { |
|
filtered = filtered.filter(r => r.points === 16); |
|
} else if (filterPoints === '0') { |
|
filtered = filtered.filter(r => r.points === 0); |
|
} else if (filterPoints !== 'all') { |
|
const minPoints = parseInt(filterPoints.replace('+', '')); |
|
filtered = filtered.filter(r => r.points >= minPoints); |
|
} |
|
|
|
return filtered; |
|
}, [gameResults, 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 stats = 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 pointsDistribution = filteredResults.reduce((acc, r) => { |
|
acc[r.points] = (acc[r.points] || 0) + 1; |
|
return acc; |
|
}, {} as Record<number, number>); |
|
|
|
return { |
|
totalGames, |
|
winningGames, |
|
totalCost, |
|
totalPrizes, |
|
netResult: totalPrizes - totalCost, |
|
winRate: (winningGames / totalGames) * 100, |
|
pointsDistribution |
|
}; |
|
}, [filteredResults]); |
|
|
|
|
|
const renderGameGrid = (result: GameResultDetailed) => { |
|
if (!showGrid) return null; |
|
|
|
return ( |
|
<div className="bg-gray-50 p-3 rounded-lg border"> |
|
<div className="text-xs font-semibold mb-2 text-gray-700"> |
|
🎯 Grid Visual - Jogo #{result.game.id} |
|
</div> |
|
|
|
{/* Grid compacto 10x10 */} |
|
<div className="grid grid-cols-10 gap-0.5"> |
|
{Array.from({ length: 100 }, (_, i) => { |
|
const number = i; |
|
const isMarked = result.numbers.includes(number); |
|
const isHit = result.matchedNumbers.includes(number); |
|
const wasDrawn = currentResult.numeros.includes(number); |
|
|
|
let cellClass = 'w-5 h-5 text-[8px] font-bold flex items-center justify-center border'; |
|
|
|
if (isMarked && isHit) { |
|
cellClass += ' bg-green-600 text-white'; // Marcado e acertou |
|
} else if (isMarked && !isHit) { |
|
cellClass += ' bg-blue-600 text-white'; // Marcado mas não acertou |
|
} else if (!isMarked && wasDrawn) { |
|
cellClass += ' bg-yellow-400 text-black'; // Não marcado mas saiu |
|
} else { |
|
cellClass += ' bg-white text-gray-600'; // Normal |
|
} |
|
|
|
return ( |
|
<div |
|
key={number} |
|
className={cellClass} |
|
title={`${number === 0 ? '00' : number} ${isMarked ? '(Marcado)' : ''} ${wasDrawn ? '(Sorteado)' : ''}`} |
|
> |
|
{number === 0 ? '00' : number.toString().padStart(2, '0')} |
|
</div> |
|
); |
|
})} |
|
</div> |
|
|
|
{/* Legenda compacta */} |
|
<div className="flex justify-between mt-2 text-[10px] text-gray-600"> |
|
<span className="flex items-center gap-1"> |
|
<div className="w-2 h-2 bg-green-600 rounded"></div> |
|
Acertou ({result.matchedNumbers.length}) |
|
</span> |
|
<span className="flex items-center gap-1"> |
|
<div className="w-2 h-2 bg-blue-600 rounded"></div> |
|
Marcado ({result.numbers.length - result.matchedNumbers.length}) |
|
</span> |
|
<span className="flex items-center gap-1"> |
|
<div className="w-2 h-2 bg-yellow-400 rounded"></div> |
|
Perdeu ({currentResult.numeros.filter(n => !result.numbers.includes(n)).length}) |
|
</span> |
|
</div> |
|
</div> |
|
); |
|
}; |
|
|
|
return ( |
|
<div className="space-y-6"> |
|
{/* Header */} |
|
<div className="bg-gradient-to-r from-green-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" /> |
|
Resultado Jogo por Jogo |
|
</h2> |
|
<p className="text-green-100 mt-2"> |
|
Análise individual de cada um dos {allGames.length} jogos contra o concurso {currentResult.concurso} |
|
</p> |
|
</div> |
|
|
|
{/* Estatísticas Resumidas */} |
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4"> |
|
<div className="bg-blue-50 p-4 rounded-lg border-l-4 border-blue-500"> |
|
<span className="text-sm text-blue-600">Jogos Analisados</span> |
|
<p className="text-2xl font-bold text-blue-800">{stats.totalGames}</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">{stats.winningGames}</p> |
|
<p className="text-xs text-green-600">{stats.winRate.toFixed(1)}%</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 Prêmios</span> |
|
<p className="text-lg font-bold text-yellow-800"> |
|
R$ {stats.totalPrizes.toLocaleString('pt-BR', { maximumFractionDigits: 0 })} |
|
</p> |
|
</div> |
|
|
|
<div className="bg-red-50 p-4 rounded-lg border-l-4 border-red-500"> |
|
<span className="text-sm text-red-600">Custo Total</span> |
|
<p className="text-lg font-bold text-red-800"> |
|
R$ {stats.totalCost.toLocaleString('pt-BR', { maximumFractionDigits: 0 })} |
|
</p> |
|
</div> |
|
|
|
<div className={`p-4 rounded-lg border-l-4 ${ |
|
stats.netResult >= 0 ? 'border-green-500 bg-green-50' : 'border-red-500 bg-red-50' |
|
}`}> |
|
<span className={`text-sm ${stats.netResult >= 0 ? 'text-green-600' : 'text-red-600'}`}> |
|
Resultado |
|
</span> |
|
<p className={`text-lg font-bold ${stats.netResult >= 0 ? 'text-green-800' : 'text-red-800'}`}> |
|
{stats.netResult >= 0 ? '+' : ''}R$ {stats.netResult.toLocaleString('pt-BR', { maximumFractionDigits: 0 })} |
|
</p> |
|
</div> |
|
</div> |
|
|
|
{/* Filtros */} |
|
<div className="bg-white p-4 rounded-lg shadow border"> |
|
<div className="flex flex-wrap gap-4 items-center"> |
|
<Filter className="w-4 h-4 text-gray-500" /> |
|
|
|
<select |
|
value={filterType} |
|
onChange={(e) => setFilterType(e.target.value as any)} |
|
className="px-3 py-1 border rounded text-sm" |
|
> |
|
<option value="all">Todos os Tipos</option> |
|
<option value="vertical">Verticais ({gameResults.filter(r => r.game.type === 'vertical').length})</option> |
|
<option value="horizontal">Horizontais ({gameResults.filter(r => r.game.type === 'horizontal').length})</option> |
|
</select> |
|
|
|
<select |
|
value={filterPoints} |
|
onChange={(e) => setFilterPoints(e.target.value as any)} |
|
className="px-3 py-1 border rounded text-sm" |
|
> |
|
<option value="all">Todas Pontuações</option> |
|
<option value="winning">Apenas Premiados</option> |
|
<option value="15+">15+ Pontos</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</option> |
|
</select> |
|
|
|
<label className="flex items-center gap-2 text-sm"> |
|
<input |
|
type="checkbox" |
|
checked={showGrid} |
|
onChange={(e) => setShowGrid(e.target.checked)} |
|
/> |
|
<Eye className="w-4 h-4" /> |
|
Grid Visual |
|
</label> |
|
|
|
<span className="text-sm text-gray-600"> |
|
{filteredResults.length} resultado(s) |
|
</span> |
|
</div> |
|
</div> |
|
|
|
{/* Lista de Resultados */} |
|
<div className="space-y-4"> |
|
{paginatedResults.map((result) => ( |
|
<div |
|
key={result.game.id} |
|
className={`p-4 border rounded-lg ${ |
|
result.isWinning ? 'border-green-300 bg-green-50' : 'border-gray-200 bg-white' |
|
}`} |
|
> |
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4"> |
|
{/* Info do Jogo */} |
|
<div> |
|
<div className="flex items-center gap-3 mb-2"> |
|
<div className="bg-blue-600 text-white px-3 py-1 rounded font-bold"> |
|
#{result.game.id} |
|
</div> |
|
<div className="text-sm text-gray-600"> |
|
<div>{result.game.type === 'vertical' ? '🔹 Vertical' : '🔸 Horizontal'}</div> |
|
<div>Fase {result.game.phase}, Ciclo {result.game.cycle}</div> |
|
</div> |
|
</div> |
|
|
|
<div className="space-y-1 text-sm"> |
|
<div className="flex justify-between"> |
|
<span>Pontuação:</span> |
|
<span className={`font-bold ${result.isWinning ? 'text-green-600' : 'text-gray-600'}`}> |
|
{result.points} pontos |
|
</span> |
|
</div> |
|
<div className="flex justify-between"> |
|
<span>Prêmio:</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="flex justify-between"> |
|
<span>Resultado:</span> |
|
<span className={`font-bold ${result.netProfit >= 0 ? 'text-green-600' : 'text-red-600'}`}> |
|
{result.netProfit >= 0 ? '+' : ''}R$ {result.netProfit.toLocaleString('pt-BR', { minimumFractionDigits: 2 })} |
|
</span> |
|
</div> |
|
{result.isWinning && ( |
|
<div className="text-xs text-green-600 mt-1"> |
|
🏆 {result.prizeDescription} |
|
</div> |
|
)} |
|
</div> |
|
</div> |
|
|
|
{/* Grid Visual */} |
|
<div> |
|
{renderGameGrid(result)} |
|
</div> |
|
|
|
{/* Números Detalhados */} |
|
<div className="space-y-3"> |
|
<div> |
|
<div className="text-sm font-semibold text-green-600 mb-1"> |
|
✅ Acertos ({result.matchedNumbers.length}) |
|
</div> |
|
<div className="flex flex-wrap gap-1"> |
|
{result.matchedNumbers.map(num => ( |
|
<span |
|
key={num} |
|
className="bg-green-600 text-white px-1.5 py-0.5 rounded text-xs font-bold" |
|
> |
|
{num === 0 ? '00' : num.toString().padStart(2, '0')} |
|
</span> |
|
))} |
|
</div> |
|
</div> |
|
|
|
<div> |
|
<div className="text-sm font-semibold text-yellow-600 mb-1"> |
|
⭐ Perdidos ({currentResult.numeros.filter(n => !result.numbers.includes(n)).length}) |
|
</div> |
|
<div className="flex flex-wrap gap-1"> |
|
{currentResult.numeros |
|
.filter(n => !result.numbers.includes(n)) |
|
.slice(0, 8) |
|
.map(num => ( |
|
<span |
|
key={num} |
|
className="bg-yellow-400 text-black px-1.5 py-0.5 rounded text-xs font-bold" |
|
> |
|
{num === 0 ? '00' : num.toString().padStart(2, '0')} |
|
</span> |
|
))} |
|
{currentResult.numeros.filter(n => !result.numbers.includes(n)).length > 8 && ( |
|
<span className="text-xs text-gray-500"> |
|
+{currentResult.numeros.filter(n => !result.numbers.includes(n)).length - 8} |
|
</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 disabled:opacity-50" |
|
> |
|
← Anterior |
|
</button> |
|
|
|
<span className="px-3 py-1 text-sm"> |
|
Página {currentPage} de {totalPages} |
|
</span> |
|
|
|
<button |
|
onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))} |
|
disabled={currentPage === totalPages} |
|
className="px-3 py-1 border rounded disabled:opacity-50" |
|
> |
|
Próximo → |
|
</button> |
|
</div> |
|
)} |
|
|
|
{/* Distribuição de Pontos */} |
|
<div className="bg-white p-4 rounded-lg shadow border"> |
|
<h3 className="font-semibold mb-3 flex items-center gap-2"> |
|
<TrendingUp className="w-4 h-4" /> |
|
Distribuição de Pontuação |
|
</h3> |
|
|
|
<div className="grid grid-cols-4 md:grid-cols-8 gap-2"> |
|
{Object.entries(stats.pointsDistribution) |
|
.sort(([a], [b]) => parseInt(b) - parseInt(a)) |
|
.map(([points, count]) => { |
|
const isWinning = parseInt(points) >= 15 || parseInt(points) === 0; |
|
|
|
return ( |
|
<div |
|
key={points} |
|
className={`p-2 rounded text-center ${ |
|
isWinning ? 'bg-green-100 border border-green-300' : 'bg-gray-100' |
|
}`} |
|
> |
|
<div className={`font-bold ${isWinning ? 'text-green-600' : 'text-gray-600'}`}> |
|
{points}pts |
|
</div> |
|
<div className="text-xs text-gray-600">{count} jogos</div> |
|
</div> |
|
); |
|
})} |
|
</div> |
|
</div> |
|
</div> |
|
); |
|
}; |
|
|
|
export default GameByGameResults; |
|
|