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:
126
apps/admin/src/composables/useAdminDashboard.ts
Normal file
126
apps/admin/src/composables/useAdminDashboard.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { ref, computed } from 'vue';
|
||||
import api from '../api';
|
||||
import type { AdminDashboard } from '../views/dashboard-types';
|
||||
import { formatAmount, formatAmountFull } from '../utils/format-amount';
|
||||
import { useAdminLocale } from './useAdminLocale';
|
||||
|
||||
const stats = ref<AdminDashboard | null>(null);
|
||||
const loading = ref(false);
|
||||
const loadError = ref(false);
|
||||
let inflight: Promise<void> | null = null;
|
||||
|
||||
export function useAdminDashboard() {
|
||||
const { t, locale, localeTag } = useAdminLocale();
|
||||
|
||||
const s = computed(() => stats.value);
|
||||
|
||||
async function load(force = false) {
|
||||
if (inflight && !force) return inflight;
|
||||
if (stats.value && !force) return;
|
||||
|
||||
loading.value = true;
|
||||
loadError.value = false;
|
||||
|
||||
inflight = (async () => {
|
||||
try {
|
||||
const { data } = await api.get('/admin/dashboard');
|
||||
stats.value = data.data as AdminDashboard;
|
||||
} catch {
|
||||
stats.value = null;
|
||||
loadError.value = true;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
inflight = null;
|
||||
}
|
||||
})();
|
||||
|
||||
return inflight;
|
||||
}
|
||||
|
||||
function fmtCount(val: number | undefined) {
|
||||
return (val ?? 0).toLocaleString(localeTag.value, { maximumFractionDigits: 0 });
|
||||
}
|
||||
|
||||
function formatTime(v: string) {
|
||||
return new Date(v).toLocaleString(localeTag.value, {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
function toNum(v: string | number | undefined) {
|
||||
const n = typeof v === 'number' ? v : parseFloat(v ?? '0');
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
}
|
||||
|
||||
function pctChange(today: string | number, yesterday: string | number) {
|
||||
const t0 = toNum(today);
|
||||
const y = toNum(yesterday);
|
||||
if (y === 0) return t0 > 0 ? '+100%' : '—';
|
||||
const p = ((t0 - y) / y) * 100;
|
||||
const sign = p > 0 ? '+' : '';
|
||||
return `${sign}${p.toFixed(1)}%`;
|
||||
}
|
||||
|
||||
const chartI18n = computed(() => ({
|
||||
locale: locale.value,
|
||||
betCountSeries: t('dash.chart_bet_count'),
|
||||
axisAmount: t('dash.axis_amount'),
|
||||
axisCount: t('dash.axis_count'),
|
||||
countSuffix: t('dash.count_suffix'),
|
||||
pieTooltip: t('dash.chart_tooltip'),
|
||||
noData: t('common.no_data'),
|
||||
pieEmpty: t('dash.pie_empty'),
|
||||
}));
|
||||
|
||||
const trendLabels = computed(() => s.value?.trend7d?.map((d) => d.label) ?? []);
|
||||
|
||||
const kpiPrimary = computed(() => {
|
||||
if (!s.value) return [];
|
||||
const td = s.value.today;
|
||||
const y = s.value.yesterday;
|
||||
const yLabel = t('common.yesterday');
|
||||
return [
|
||||
{
|
||||
label: t('dash.kpi_bet_count'),
|
||||
value: fmtCount(td.betCount),
|
||||
sub: `${yLabel} ${fmtCount(y.betCount)}`,
|
||||
delta: pctChange(td.betCount, y.betCount),
|
||||
},
|
||||
{
|
||||
label: t('dash.kpi_stake'),
|
||||
value: formatAmount(td.stake, 2, locale.value),
|
||||
sub: formatAmountFull(td.stake, locale.value),
|
||||
delta: pctChange(td.stake, y.stake),
|
||||
},
|
||||
{
|
||||
label: t('dash.kpi_payout'),
|
||||
value: formatAmount(td.payout, 2, locale.value),
|
||||
sub: `${yLabel} ${formatAmount(y.payout, 2, locale.value)}`,
|
||||
delta: pctChange(td.payout, y.payout),
|
||||
},
|
||||
{
|
||||
label: t('dash.kpi_ggr'),
|
||||
value: formatAmount(td.ggr, 2, locale.value),
|
||||
sub: `${yLabel} ${formatAmount(y.ggr, 2, locale.value)}`,
|
||||
delta: pctChange(td.ggr, y.ggr),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
return {
|
||||
stats,
|
||||
s,
|
||||
loading,
|
||||
loadError,
|
||||
load,
|
||||
fmtCount,
|
||||
formatTime,
|
||||
toNum,
|
||||
chartI18n,
|
||||
trendLabels,
|
||||
kpiPrimary,
|
||||
};
|
||||
}
|
||||
@@ -63,7 +63,7 @@ export function useAdminPlayerTransfer(onSuccess?: () => void | Promise<void>) {
|
||||
transferTarget.value = row;
|
||||
transferContext.value = null;
|
||||
transferAmount.value = 100;
|
||||
transferRemark.value = type === 'deposit' ? t('user.deposit_remark_default') : '';
|
||||
transferRemark.value = type === 'deposit' ? t('user.deposit_remark_default') : t('user.withdraw_remark_default');
|
||||
transferVisible.value = true;
|
||||
transferContextLoading.value = true;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user