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>
127 lines
3.4 KiB
TypeScript
127 lines
3.4 KiB
TypeScript
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,
|
|
};
|
|
}
|