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:
2026-06-10 13:00:14 +08:00
parent 6124313369
commit 03f54ca689
43 changed files with 2787 additions and 519 deletions

View File

@@ -168,11 +168,6 @@ export class OutrightService {
this.getOutrightTitle(match.id, 'ms-MY'),
]);
const addableFixtureTeams = await this.listAddableFixtureTeams(
match.leagueId,
fullMarket.id,
);
return {
id: match.id.toString(),
leagueId: match.leagueId.toString(),
@@ -192,49 +187,9 @@ export class OutrightService {
playerVisible: visibility.playerVisible,
playerHiddenReason: visibility.playerHiddenReason,
selections,
addableFixtureTeams,
};
}
private async listAddableFixtureTeams(leagueId: bigint, marketId: bigint) {
const teams = await this.collectFixtureTeamsForLeague(leagueId);
const openCodes = new Set(
(
await this.prisma.marketSelection.findMany({
where: {
marketId,
status: 'OPEN',
selectionCode: { not: PLACEHOLDER_TEAM_CODE },
},
select: { selectionCode: true },
})
).map((s) => s.selectionCode),
);
const result: Array<{
teamCode: string;
teamZh: string;
teamEn: string;
logoUrl: string | null;
}> = [];
for (const team of teams) {
if (openCodes.has(team.code)) continue;
const [teamZh, teamEn] = await Promise.all([
this.getTranslation('TEAM', team.id, 'zh-CN'),
this.getTranslation('TEAM', team.id, 'en-US'),
]);
result.push({
teamCode: team.code,
teamZh: teamZh || team.code,
teamEn: teamEn || team.code,
logoUrl: team.logoUrl ?? null,
});
}
return result;
}
/** 按联赛获取或创建冠军盘,并从单场赛程同步参赛队伍 */
async getOrCreateAndSyncForLeague(leagueId: bigint) {
let match = await this.prisma.match.findFirst({
@@ -263,6 +218,22 @@ export class OutrightService {
orderBy: { id: 'asc' },
});
}
const sync = await this.syncSelectionsFromLeagueFixtures(match.id);
const data = await this.getForAdmin(match.id);
return {
...data,
fixtureSyncAdded: sync.addedCount,
fixtureSyncReopened: sync.reopenedCount,
};
}
/** 若联赛已有冠军盘,则从单场同步球队(不自动创建冠军盘) */
async syncOutrightTeamsForLeagueIfExists(leagueId: bigint) {
const match = await this.prisma.match.findFirst({
where: { leagueId, isOutright: true, deletedAt: null },
orderBy: { id: 'asc' },
});
if (!match) return { addedCount: 0, reopenedCount: 0 };
return this.syncSelectionsFromLeagueFixtures(match.id);
}
@@ -279,19 +250,21 @@ export class OutrightService {
},
});
// 仅当球队重新出现在单场赛程时,恢复曾被关闭的选项;不因「暂无单场」而自动关闭
const existingCodes = new Set(existing.map((s) => s.selectionCode));
let sortOrder = existing.reduce((max, s) => Math.max(max, s.sortOrder), -1);
let addedCount = 0;
let reopenedCount = 0;
for (const sel of existing) {
if (fixtureCodes.has(sel.selectionCode) && sel.status === 'CLOSED') {
await this.prisma.marketSelection.update({
where: { id: sel.id },
data: { status: 'OPEN' },
});
reopenedCount += 1;
}
}
const existingCodes = new Set(existing.map((s) => s.selectionCode));
let sortOrder = existing.reduce((max, s) => Math.max(max, s.sortOrder), -1);
for (const team of teams) {
if (existingCodes.has(team.code)) continue;
const [teamZh, teamEn] = await Promise.all([
@@ -309,9 +282,10 @@ export class OutrightService {
status: 'OPEN',
},
});
addedCount += 1;
}
return this.getForAdmin(matchId);
return { addedCount, reopenedCount };
}
private async collectFixtureTeamsForLeague(leagueId: bigint) {