Files
Bookie/BookieClient/src/app/features/pre-match/pre-match.component.html
2026-09-17 21:45:57 +02:00

422 lines
21 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<div class="page">
<div class="page-header">
<h1>Pre-Match Reports</h1>
<div class="toolbar-row" style="margin: 0;">
<mat-form-field appearance="outline" class="date-field">
<mat-label>Report date</mat-label>
<input matInput [matDatepicker]="picker" [formControl]="dateControl" (dateChange)="load()" />
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="seasons-field">
<mat-label>History depth</mat-label>
<mat-select [formControl]="seasonsBackControl" (selectionChange)="load()">
@for (opt of seasonsBackOptions; track opt.value) {
<mat-option [value]="opt.value">{{ opt.label }}</mat-option>
}
</mat-select>
</mat-form-field>
<button mat-flat-button color="primary" (click)="load()">
<mat-icon>refresh</mat-icon> Refresh
</button>
<button mat-stroked-button (click)="exportCsv()" [disabled]="!hasMatches()">
<mat-icon>table_view</mat-icon> CSV
</button>
<button mat-stroked-button (click)="exportPdf()" [disabled]="!hasMatches()">
<mat-icon>picture_as_pdf</mat-icon> PDF
</button>
<button mat-flat-button class="analyze-btn" (click)="analyze()"
[disabled]="analyzableCount() === 0 || analyzing()"
[matTooltip]="analyzableCount() === 0
? 'Requires finished matches with LLM, OpenAI and actual stats for both teams'
: 'Generate analysis PDF for ' + analyzableCount() + ' match(es)'">
@if (analyzing()) {
<mat-icon class="spin">hourglass_top</mat-icon>
} @else {
<mat-icon>analytics</mat-icon>
}
Analyze
@if (analyzableCount() > 0) {
<span class="analyze-count">({{ analyzableCount() }})</span>
}
</button>
<button mat-flat-button class="predict-selected-btn" (click)="predictSelected()"
[disabled]="selectedCount() === 0"
[matBadge]="selectedCount()" [matBadgeHidden]="selectedCount() === 0" matBadgeColor="accent">
<mat-icon>insights</mat-icon> Predict selected
</button>
</div>
</div>
<!-- Loading skeleton -->
@if (loading()) {
<div class="skeleton-list">
@for (s of skeletons; track s) {
<mat-card class="skeleton-card">
<div class="sk sk-title"></div>
<div class="sk sk-line"></div>
<div class="sk sk-line short"></div>
<div class="sk-panels">
<div class="sk sk-panel"></div>
<div class="sk sk-panel"></div>
</div>
</mat-card>
}
</div>
} @else if (errored()) {
<mat-card class="state-card">
<mat-icon class="state-icon">error_outline</mat-icon>
<h2>Could not load the report</h2>
<p class="muted">The API request failed. Check that the backend is running and try again.</p>
<button mat-flat-button color="primary" (click)="load()">Retry</button>
</mat-card>
} @else if (!hasMatches()) {
<mat-card class="state-card">
<mat-icon class="state-icon">event_busy</mat-icon>
<h2>No matches on this date</h2>
<p class="muted">There are no fixtures scheduled for {{ dateControl.value | date: 'fullDate' }}.</p>
</mat-card>
} @else {
<div class="result-count">
<span class="muted">{{ report()!.matchCount }} match(es) on {{ report()!.date }}</span>
<span class="select-tools">
@if (selectedCount() > 0) {
<span class="muted small">{{ selectedCount() }} selected</span>
<button mat-button (click)="clearSelection()">Clear</button>
}
<button mat-button (click)="selectAll()">Select all</button>
</span>
</div>
@for (group of report()!.groups; track group.leagueId + '-' + group.seasonId) {
<mat-expansion-panel class="league-panel" [expanded]="true">
<mat-expansion-panel-header>
<mat-panel-title>
<mat-icon class="flag-icon">emoji_events</mat-icon>
{{ group.leagueName }}
<span class="muted country">&nbsp;— {{ group.country }}</span>
</mat-panel-title>
<mat-panel-description>
Season {{ group.seasonName }} · {{ group.matches.length }} match(es)
</mat-panel-description>
</mat-expansion-panel-header>
@if (group.modelParams) {
<div class="league-model-bar">
<mat-icon>model_training</mat-icon>
<span class="model-label">Dixon-Coles model</span>
<span class="model-chip">HA {{ group.modelParams.homeAdvantage }}</span>
<span class="model-chip">ρ {{ group.modelParams.rho }}</span>
@if (group.modelParams.avgHomeGoals != null) {
<span class="model-chip">avg H {{ group.modelParams.avgHomeGoals }}</span>
}
@if (group.modelParams.avgAwayGoals != null) {
<span class="model-chip">avg A {{ group.modelParams.avgAwayGoals }}</span>
}
<span class="muted small">
{{ group.modelParams.matchesUsed }} matches
@if (group.modelParams.halfLifeDays) { · half-life {{ group.modelParams.halfLifeDays }}d }
· trained {{ group.modelParams.trainedAt | date: 'mediumDate' }}
</span>
</div>
}
@for (m of group.matches; track m.matchId) {
<mat-card class="match-card" [class.selected]="isSelected(m.matchId)">
<div class="match-head">
<mat-checkbox class="select-box" [checked]="isSelected(m.matchId)"
(change)="toggleSelected(m.matchId)"
matTooltip="Select for batch prediction"></mat-checkbox>
<div class="kickoff mono">{{ m.kickoffAt | date: 'HH:mm' }}</div>
<div class="teams">
<span class="team-name">{{ m.homeTeamName }}</span>
@if (m.hasScore) {
<span class="score mono">{{ m.homeScoreFt }} : {{ m.awayScoreFt }}</span>
} @else {
<span class="vs">vs</span>
}
<span class="team-name">{{ m.awayTeamName }}</span>
</div>
<span class="status-badge" [class]="statusClass(m.status)">
{{ statusLabels[m.status] ?? m.status }}
</span>
</div>
@if (m.stadium || m.referee) {
<div class="match-meta muted">
@if (m.stadium) { <span><mat-icon>stadium</mat-icon>{{ m.stadium }}</span> }
@if (m.referee) { <span><mat-icon>sports</mat-icon>{{ m.referee }}</span> }
@if (m.hasScore) { <span><mat-icon>schedule</mat-icon>HT {{ m.homeScoreHt }}:{{ m.awayScoreHt }}</span> }
</div>
}
<div class="match-actions">
<span class="history-note muted">
<mat-icon>history_toggle_off</mat-icon>{{ seasonsLabel(m) }}
</span>
<span class="action-buttons">
<button mat-stroked-button color="primary" (click)="openHeadToHead(m)">
<mat-icon>compare_arrows</mat-icon> Head-to-head
</button>
<button mat-flat-button class="predict-btn" (click)="openPrediction(m)">
<mat-icon>insights</mat-icon> Predict
</button>
</span>
</div>
@if (m.odds) {
<div class="odds-block">
<span class="muted small">
<mat-icon>casino</mat-icon>
Market 1X2 (avg {{ m.odds.bookmakerCount }} bookmaker{{ m.odds.bookmakerCount === 1 ? '' : 's' }})
</span>
<div class="odds-grid">
<div class="odds-cell home-odds">
<span class="muted">Home</span>
<b class="mono">{{ m.odds.avgHomeOdds | number: '1.2-2' }}</b>
<span class="implied">{{ (m.odds.homeImplied * 100) | number: '1.1-1' }}%</span>
</div>
<div class="odds-cell draw-odds">
<span class="muted">Draw</span>
<b class="mono">{{ m.odds.avgDrawOdds | number: '1.2-2' }}</b>
<span class="implied">{{ (m.odds.drawImplied * 100) | number: '1.1-1' }}%</span>
</div>
<div class="odds-cell away-odds">
<span class="muted">Away</span>
<b class="mono">{{ m.odds.avgAwayOdds | number: '1.2-2' }}</b>
<span class="implied">{{ (m.odds.awayImplied * 100) | number: '1.1-1' }}%</span>
</div>
</div>
@if (m.odds.avgTotalLine != null || m.odds.avgOverOdds != null || m.odds.avgUnderOdds != null) {
<div class="totals-row">
<span class="muted small">Totals O/U</span>
@if (m.odds.avgTotalLine != null) {
<span class="totals-chip">Line <b class="mono">{{ m.odds.avgTotalLine }}</b></span>
}
@if (m.odds.avgOverOdds != null) {
<span class="totals-chip">
Over <b class="mono">{{ m.odds.avgOverOdds | number: '1.2-2' }}</b>
@if (m.odds.overImplied != null) {
<span class="implied">{{ (m.odds.overImplied * 100) | number: '1.1-1' }}%</span>
}
</span>
}
@if (m.odds.avgUnderOdds != null) {
<span class="totals-chip">
Under <b class="mono">{{ m.odds.avgUnderOdds | number: '1.2-2' }}</b>
@if (m.odds.underImplied != null) {
<span class="implied">{{ (m.odds.underImplied * 100) | number: '1.1-1' }}%</span>
}
</span>
}
</div>
}
</div>
}
@if (m.extraOdds?.length) {
<div class="extra-odds-block">
<span class="muted small"><mat-icon>stacked_line_chart</mat-icon> Extra markets</span>
<table class="extra-odds-table">
<thead>
<tr>
<th>Market</th>
<th>Bookmaker</th>
<th class="num-col">Line</th>
<th class="num-col">Over</th>
<th class="num-col">Under</th>
</tr>
</thead>
<tbody>
@for (eo of m.extraOdds; track eo.market + eo.bookmaker) {
<tr>
<td>{{ extraMarketLabel(eo.market) }}</td>
<td>{{ eo.bookmaker }}</td>
<td class="mono num-col">{{ eo.line }}</td>
<td class="mono num-col">{{ eo.overOdds | number: '1.2-2' }}</td>
<td class="mono num-col">{{ eo.underOdds | number: '1.2-2' }}</td>
</tr>
}
</tbody>
</table>
</div>
}
<div class="team-panels">
@for (t of [m.home, m.away]; track t.teamId) {
<div class="team-panel">
<div class="team-panel-head">
<span class="team-panel-name">{{ t.teamName }}</span>
<span class="muted">based on {{ t.matchesPlayed }} earlier match(es)</span>
</div>
@if (t.strength) {
<div class="strength-row">
<mat-icon>fitness_center</mat-icon>
<span>
Atk <b class="mono">{{ t.strength.attackFactor }}</b>
· Def <b class="mono">{{ t.strength.defenseFactor }}</b>
<span class="muted small">({{ t.strength.matchesUsed }} matches)</span>
</span>
</div>
}
@if (t.oddsApiName) {
<div class="alias-row muted small">
<mat-icon>link</mat-icon> Odds API: {{ t.oddsApiName }}
</div>
}
@if (!t.hasHistory) {
<div class="no-history">
<mat-icon>info</mat-icon> No historical data in the selected window.
</div>
} @else {
<div class="stat-row goals">
<mat-icon>sports_soccer</mat-icon>
<span>{{ goalsSummary(t) }}</span>
</div>
@if (t.seasonAverages.hasData) {
<div class="averages-grid">
<div class="avg"><span class="muted">Possession</span><b>{{ t.seasonAverages.possession }}%</b></div>
<div class="avg"><span class="muted">Shots</span><b>{{ t.seasonAverages.shotsTotal }}</b></div>
<div class="avg"><span class="muted">On target</span><b>{{ t.seasonAverages.shotsOnTarget }}</b></div>
<div class="avg"><span class="muted">Corners</span><b>{{ t.seasonAverages.corners }}</b></div>
<div class="avg"><span class="muted">Fouls</span><b>{{ t.seasonAverages.fouls }}</b></div>
<div class="avg"><span class="muted">Offsides</span><b>{{ t.seasonAverages.offsides }}</b></div>
<div class="avg"><span class="muted">Yellow</span><b>{{ t.seasonAverages.yellowCards }}</b></div>
<div class="avg"><span class="muted">Red</span><b>{{ t.seasonAverages.redCards }}</b></div>
</div>
} @else {
<div class="muted small">Detailed match stats: no data.</div>
}
<div class="form-row">
<span class="muted">Form:</span>
@if (t.form.length) {
@for (r of t.form; track $index) {
<button type="button" class="form-chip" [class]="chipClass(r.result)"
[matTooltip]="formTooltip(r)" matTooltipPosition="above"
(click)="openMatch(r.matchId)">{{ r.result }}</button>
}
} @else {
<span class="muted small">no data</span>
}
</div>
<div class="scorers">
<span class="muted small">Top scorers (goals + assists so far)</span>
@if (t.topScorers.length) {
<table class="scorer-table">
<tbody>
@for (s of t.topScorers; track s.playerName) {
<tr>
<td class="scorer-name">{{ s.playerName }}</td>
<td class="mono">{{ s.goals }}G</td>
<td class="mono">{{ s.assists }}A</td>
</tr>
}
</tbody>
</table>
} @else {
<div class="muted small">no data</div>
}
</div>
}
@if (t.prediction || t.openAiPrediction || t.actual?.hasData) {
<div class="pred-block">
<span class="muted small">
@if (t.actual?.hasData && (t.prediction || t.openAiPrediction)) {
Predictions vs actual
} @else if (t.actual?.hasData) {
Match statistics
} @else {
Predictions (per this match)
}
</span>
<table class="pred-table">
<thead>
<tr>
<th>Metric</th>
@if (t.prediction) { <th class="num-col llm-col">LLM</th> }
@if (t.openAiPrediction) { <th class="num-col openai-col">OpenAI</th> }
@if (t.actual?.hasData) { <th class="num-col actual-col">Actual</th> }
</tr>
</thead>
<tbody>
<tr>
<td>Goals</td>
@if (t.prediction) { <td class="mono num-col">{{ t.prediction.predictedGoals ?? '—' }}</td> }
@if (t.openAiPrediction) { <td class="mono num-col openai-val">{{ t.openAiPrediction.predictedGoals ?? '—' }}</td> }
@if (t.actual?.hasData) { <td class="mono num-col actual-val">{{ t.actual!.goals ?? '—' }}</td> }
</tr>
@if (t.prediction || t.actual?.shotsTotal != null) {
<tr>
<td>Shots</td>
@if (t.prediction) { <td class="mono num-col">{{ t.prediction.predictedShotsTotal ?? '—' }}</td> }
@if (t.openAiPrediction) { <td class="mono num-col openai-val"></td> }
@if (t.actual?.hasData) { <td class="mono num-col actual-val">{{ t.actual!.shotsTotal ?? '—' }}</td> }
</tr>
}
<tr>
<td>On target</td>
@if (t.prediction) { <td class="mono num-col">{{ t.prediction.predictedShotsOnTarget ?? '—' }}</td> }
@if (t.openAiPrediction) { <td class="mono num-col openai-val">{{ t.openAiPrediction.predictedShotsOnTarget ?? '—' }}</td> }
@if (t.actual?.hasData) { <td class="mono num-col actual-val">{{ t.actual!.shotsOnTarget ?? '—' }}</td> }
</tr>
<tr>
<td>Corners</td>
@if (t.prediction) { <td class="mono num-col">{{ t.prediction.predictedCorners ?? '—' }}</td> }
@if (t.openAiPrediction) { <td class="mono num-col openai-val">{{ t.openAiPrediction.predictedCorners ?? '—' }}</td> }
@if (t.actual?.hasData) { <td class="mono num-col actual-val">{{ t.actual!.corners ?? '—' }}</td> }
</tr>
<tr>
<td>Fouls</td>
@if (t.prediction) { <td class="mono num-col">{{ t.prediction.predictedFouls ?? '—' }}</td> }
@if (t.openAiPrediction) { <td class="mono num-col openai-val">{{ t.openAiPrediction.predictedFouls ?? '—' }}</td> }
@if (t.actual?.hasData) { <td class="mono num-col actual-val">{{ t.actual!.fouls ?? '—' }}</td> }
</tr>
<tr>
<td>Yellow cards</td>
@if (t.prediction) { <td class="mono num-col">{{ t.prediction.predictedYellowCards ?? '—' }}</td> }
@if (t.openAiPrediction) { <td class="mono num-col openai-val">{{ t.openAiPrediction.predictedYellowCards ?? '—' }}</td> }
@if (t.actual?.hasData) { <td class="mono num-col actual-val">{{ t.actual!.yellowCards }}</td> }
</tr>
@if (t.openAiPrediction || t.actual?.hasData) {
<tr>
<td>Red cards</td>
@if (t.prediction) { <td class="mono num-col"></td> }
@if (t.openAiPrediction) { <td class="mono num-col openai-val">{{ t.openAiPrediction.predictedRedCards ?? '—' }}</td> }
@if (t.actual?.hasData) { <td class="mono num-col actual-val">{{ t.actual!.redCards }}</td> }
</tr>
}
</tbody>
</table>
@if (t.openAiPrediction) {
<div class="openai-meta muted small">
@if (t.openAiPrediction.model) { <span>{{ t.openAiPrediction.model }}</span> }
@if (t.openAiPrediction.confidence) { <span>· {{ t.openAiPrediction.confidence }} confidence</span> }
@if (t.openAiPrediction.predictedAt) { <span>· {{ t.openAiPrediction.predictedAt | date: 'medium' }}</span> }
</div>
}
@if (t.prediction) {
<div class="llm-meta muted small">
<span>LLM model</span>
@if (t.prediction.halfLifeDays) { <span>· half-life {{ t.prediction.halfLifeDays }}d</span> }
<span>· trained {{ t.prediction.modelTrainedAt | date: 'mediumDate' }}</span>
<span>· saved {{ t.prediction.predictedAt | date: 'short' }}</span>
</div>
}
</div>
}
</div>
}
</div>
</mat-card>
}
</mat-expansion-panel>
}
}
</div>