feat: split admin dashboard, improve match ops, and player closed-match UX
Admin: add match/player overview sub-nav; refine settlement flow and league match management UI; improve action button enabled/disabled styles; enhance logo upload and outright odds sync. API: expose matchPhase/bettingOpen for closed matches; league publish guards; settlement preview with auto score save; outright team auto-sync. Player: watermark for closed/settled states; keep match and bet details visible; remove default login credentials. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -19,6 +19,8 @@ import { usePullToRefresh } from '../composables/usePullToRefresh';
|
||||
import vsImg from '../assets/images/vs.png';
|
||||
import GoldSpinner from '../components/GoldSpinner.vue';
|
||||
import BetSuccessOverlay from '../components/BetSuccessOverlay.vue';
|
||||
import StatusWatermark from '../components/StatusWatermark.vue';
|
||||
import { matchPhaseLabel, matchPhaseVariant, type MatchPhase } from '../utils/matchPhase';
|
||||
import cardBg from '../assets/images/card-bg.png';
|
||||
|
||||
const heroCardBg = `url(${cardBg})`;
|
||||
@@ -59,6 +61,15 @@ interface MatchDetail {
|
||||
startTime: string;
|
||||
stage?: string | null;
|
||||
groupName?: string | null;
|
||||
status?: string;
|
||||
bettingOpen?: boolean;
|
||||
matchPhase?: MatchPhase;
|
||||
score?: {
|
||||
htHome: number;
|
||||
htAway: number;
|
||||
ftHome: number;
|
||||
ftAway: number;
|
||||
} | null;
|
||||
markets: Market[];
|
||||
}
|
||||
|
||||
@@ -96,6 +107,21 @@ const kickoff = computed(() => {
|
||||
});
|
||||
});
|
||||
|
||||
const bettingOpen = computed(() => match.value?.bettingOpen !== false);
|
||||
|
||||
const matchPhase = computed(
|
||||
(): MatchPhase =>
|
||||
match.value?.matchPhase ?? (bettingOpen.value ? 'open' : 'closed_pending'),
|
||||
);
|
||||
|
||||
const phaseLabel = computed(() => matchPhaseLabel(t, matchPhase.value));
|
||||
|
||||
const liveScoreText = computed(() => {
|
||||
const s = match.value?.score;
|
||||
if (!s) return '';
|
||||
return `${s.ftHome} - ${s.ftAway}`;
|
||||
});
|
||||
|
||||
function marketLabel(marketType: string) {
|
||||
const key = MARKET_I18N_KEY[marketType];
|
||||
return key ? t(key) : marketType;
|
||||
@@ -170,6 +196,7 @@ async function confirmCorrectScoreBets() {
|
||||
}
|
||||
|
||||
async function placeCorrectScoreBets(marketType: string) {
|
||||
if (!bettingOpen.value) return;
|
||||
const market = marketsByType.value.get(marketType);
|
||||
if (!market || !match.value) return;
|
||||
const entries = market.selections.filter((s) => (correctScoreStakes.value[s.id] ?? 0) > 0);
|
||||
@@ -228,7 +255,7 @@ function isSelected(id: string) {
|
||||
}
|
||||
|
||||
function toggleSelection(sel: Selection, market: Market) {
|
||||
if (!match.value) return;
|
||||
if (!match.value || !bettingOpen.value) return;
|
||||
slip.addItem({
|
||||
selectionId: sel.id,
|
||||
oddsVersion: String(sel.oddsVersion),
|
||||
@@ -295,7 +322,13 @@ function hasSlipPickForMarket(marketType: string) {
|
||||
<GoldSpinner :size="36" />
|
||||
</div>
|
||||
<template v-else-if="match">
|
||||
<section class="match-hero">
|
||||
<section class="match-hero" :class="{ 'match-hero--phase': matchPhase !== 'open' }">
|
||||
<StatusWatermark
|
||||
v-if="matchPhase !== 'open'"
|
||||
:label="phaseLabel"
|
||||
:variant="matchPhaseVariant(matchPhase)"
|
||||
size="lg"
|
||||
/>
|
||||
<div class="hero-teams">
|
||||
<!-- home -->
|
||||
<div class="hero-team">
|
||||
@@ -348,6 +381,7 @@ function hasSlipPickForMarket(marketType: string) {
|
||||
</div>
|
||||
|
||||
<p class="kickoff">{{ t('bet.kickoff_time') }}{{ kickoff }}</p>
|
||||
<p v-if="liveScoreText" class="live-score">{{ liveScoreText }}</p>
|
||||
</section>
|
||||
|
||||
<section class="markets-section">
|
||||
@@ -379,14 +413,22 @@ function hasSlipPickForMarket(marketType: string) {
|
||||
@toggle="toggleMarket(marketType)"
|
||||
/>
|
||||
<template v-if="isExpanded(marketType) && marketsByType.get(marketType)">
|
||||
<div class="market-panel-wrap" :class="{ locked: !bettingOpen }">
|
||||
<StatusWatermark
|
||||
v-if="!bettingOpen"
|
||||
:label="phaseLabel"
|
||||
:variant="matchPhaseVariant(matchPhase)"
|
||||
size="md"
|
||||
/>
|
||||
<CorrectScorePanel
|
||||
v-if="isCorrectScoreMarket(marketType)"
|
||||
:market-type="marketType"
|
||||
:selections="marketsByType.get(marketType)!.selections"
|
||||
:locked="!bettingOpen"
|
||||
v-model:stakes="correctScoreStakes"
|
||||
/>
|
||||
<button
|
||||
v-if="isCorrectScoreMarket(marketType)"
|
||||
v-if="isCorrectScoreMarket(marketType) && bettingOpen"
|
||||
type="button"
|
||||
class="market-foot-btn"
|
||||
@click="openCorrectScoreConfirm(marketType)"
|
||||
@@ -396,18 +438,20 @@ function hasSlipPickForMarket(marketType: string) {
|
||||
<MarketSelectionsPanel
|
||||
v-else
|
||||
compact
|
||||
:locked="!bettingOpen"
|
||||
:selections="marketsByType.get(marketType)!.selections"
|
||||
:is-selected="isSelected"
|
||||
@pick="onPickSelection($event, marketType)"
|
||||
/>
|
||||
<button
|
||||
v-if="!isCorrectScoreMarket(marketType) && hasSlipPickForMarket(marketType)"
|
||||
v-if="!isCorrectScoreMarket(marketType) && bettingOpen && hasSlipPickForMarket(marketType)"
|
||||
type="button"
|
||||
class="market-foot-btn"
|
||||
@click="openBetSlipDrawer"
|
||||
>
|
||||
{{ t('bet.cs_confirm_cell') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -501,6 +545,19 @@ function hasSlipPickForMarket(marketType: string) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.match-hero--phase {
|
||||
opacity: 0.98;
|
||||
}
|
||||
|
||||
.market-panel-wrap {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.market-panel-wrap.locked {
|
||||
opacity: 0.82;
|
||||
}
|
||||
|
||||
.kickoff {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
@@ -511,6 +568,17 @@ function hasSlipPickForMarket(marketType: string) {
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
.live-score {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
margin: 8px 0 0;
|
||||
font-size: 28px;
|
||||
font-weight: 900;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.hero-teams {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
Reference in New Issue
Block a user