feat: add smoke tests, agent credit ledger, and player cashback page

Introduce admin smoke-test suite with API probes, agent credit transaction history, and player cashback records; fix SmokeTestModule DI and polish admin/player UI assets.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-09 16:05:48 +08:00
parent 9c6c5e51f3
commit d5e7c8edb3
52 changed files with 3357 additions and 67 deletions

View File

@@ -0,0 +1,223 @@
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useAdminLocale } from '../composables/useAdminLocale';
import api from '../api';
import AdminTableEmpty from '../components/AdminTableEmpty.vue';
import { formatAmount, formatAmountFull } from '../utils/format-amount';
const { t, locale, localeTag } = useAdminLocale();
const route = useRoute();
const router = useRouter();
interface CreditTxRow {
id: string;
agentId: string;
agentUsername: string | null;
transactionType: string;
amount: string;
creditBefore: string;
creditAfter: string;
operatorUsername: string | null;
remark: string | null;
createdAt: string;
}
const items = ref<CreditTxRow[]>([]);
const total = ref(0);
const page = ref(1);
const pageSize = ref(20);
const keyword = ref('');
const agentId = ref('');
const transactionType = ref('');
function creditTypeLabel(type: string) {
if (type === 'CREDIT_INCREASE') return t('agent.credit.increase');
if (type === 'CREDIT_DECREASE') return t('agent.credit.decrease');
return type;
}
function formatTime(v: string) {
return new Date(v).toLocaleString(localeTag.value, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
}
async function load() {
const { data } = await api.get('/admin/agents/credit-transactions', {
params: {
page: page.value,
pageSize: pageSize.value,
keyword: keyword.value.trim() || undefined,
agentId: agentId.value.trim() || undefined,
transactionType: transactionType.value || undefined,
},
});
items.value = (data.data?.items ?? []) as CreditTxRow[];
total.value = data.data?.total ?? 0;
}
function onSearch() {
page.value = 1;
void load();
}
function onPageChange(p: number) {
page.value = p;
void load();
}
function onSizeChange(size: number) {
pageSize.value = size;
page.value = 1;
void load();
}
function openAgentFilter(id: string) {
agentId.value = id;
keyword.value = '';
page.value = 1;
void router.replace({ query: { agentId: id } });
void load();
}
onMounted(() => {
const q = route.query.agentId;
if (typeof q === 'string' && q.trim()) {
agentId.value = q.trim();
}
void load();
});
watch(
() => route.query.agentId,
(q) => {
const next = typeof q === 'string' ? q.trim() : '';
if (next !== agentId.value) {
agentId.value = next;
page.value = 1;
void load();
}
},
);
</script>
<template>
<div class="admin-list-page">
<el-card class="filter-card" shadow="never">
<el-form inline>
<el-form-item :label="t('common.keyword')">
<el-input
v-model="keyword"
:placeholder="t('agent.credit_tx.filter_agent_ph')"
clearable
style="width: 180px"
@keyup.enter="onSearch"
/>
</el-form-item>
<el-form-item :label="t('agent.credit_tx.filter_agent_id')">
<el-input
v-model="agentId"
:placeholder="t('agent.credit_tx.filter_agent_id_ph')"
clearable
style="width: 140px"
@keyup.enter="onSearch"
/>
</el-form-item>
<el-form-item :label="t('agent.col.credit_type')">
<el-select v-model="transactionType" clearable :placeholder="t('common.all')" style="width: 120px">
<el-option :label="t('agent.credit.increase')" value="CREDIT_INCREASE" />
<el-option :label="t('agent.credit.decrease')" value="CREDIT_DECREASE" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSearch">{{ t('common.search') }}</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="data-card" shadow="never">
<div class="table-wrap">
<el-table :key="locale" :data="items" stripe>
<template #empty>
<AdminTableEmpty />
</template>
<el-table-column :label="t('audit.col.time')" min-width="158">
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
</el-table-column>
<el-table-column :label="t('user.col.username')" min-width="120">
<template #default="{ row }">
<el-button
v-if="row.agentUsername"
link
type="primary"
@click="openAgentFilter(row.agentId)"
>
{{ row.agentUsername }}
</el-button>
<span v-else>{{ row.agentId }}</span>
</template>
</el-table-column>
<el-table-column :label="t('agent.col.credit_type')" width="88">
<template #default="{ row }">{{ creditTypeLabel(row.transactionType) }}</template>
</el-table-column>
<el-table-column :label="t('agent.col.credit_change')" width="108" align="right">
<template #default="{ row }">
<el-tooltip :content="formatAmountFull(row.amount)" placement="top">
<span :class="parseFloat(row.amount) >= 0 ? 'amt-pos' : 'amt-neg'">
{{ formatAmount(row.amount) }}
</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column :label="t('agent.col.credit_before')" width="108" align="right">
<template #default="{ row }">
<el-tooltip :content="formatAmountFull(row.creditBefore)" placement="top">
<span>{{ formatAmount(row.creditBefore) }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column :label="t('agent.col.credit_after')" width="108" align="right">
<template #default="{ row }">
<el-tooltip :content="formatAmountFull(row.creditAfter)" placement="top">
<span>{{ formatAmount(row.creditAfter) }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column :label="t('agent.credit_tx.col.operator')" min-width="100">
<template #default="{ row }">{{ row.operatorUsername ?? '—' }}</template>
</el-table-column>
<el-table-column prop="remark" :label="t('user.field.remark')" min-width="140" show-overflow-tooltip />
</el-table>
</div>
<div class="pager">
<el-pagination
v-model:current-page="page"
v-model:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@current-change="onPageChange"
@size-change="onSizeChange"
/>
</div>
</el-card>
</div>
</template>
<style scoped>
.amt-pos {
color: var(--el-color-success);
font-weight: 600;
}
.amt-neg {
color: var(--el-color-danger);
font-weight: 600;
}
</style>

View File

@@ -19,14 +19,14 @@ import {
type PlayerDetail,
type PlayerCreateForm,
type PlayerEditForm,
} from './user-form.ts';
} from './user-form';
import {
emptyAgentEditForm,
editFormFromAgentDetail,
type AgentRow,
type AgentDetail,
type AgentEditForm,
} from './agent-form.ts';
} from './agent-form';
import { subAgentAccountStatus } from './agent/agent-sub-agent-form';
import {
formatAmount,
@@ -994,8 +994,8 @@ function creditTypeLabel(type: string) {
:expand-row-keys="subAgentExpandedKeys"
:row-class-name="expandableTableRowClassName"
class="inner-table expandable-table"
@expand-change="(sub, rows) => onSubAgentExpand(row.userId, sub, rows)"
@row-click="(sub, col, e) => onSubAgentRowClick(row.userId, sub, col, e)"
@expand-change="(sub: AgentRow, rows: AgentRow[]) => onSubAgentExpand(row.userId, sub, rows)"
@row-click="(sub: AgentRow, col: unknown, e: MouseEvent) => onSubAgentRowClick(row.userId, sub, col, e)"
>
<template #empty><AdminTableEmpty /></template>
<el-table-column type="expand">
@@ -1469,7 +1469,17 @@ function creditTypeLabel(type: string) {
<el-descriptions-item :label="t('agent.col.created')" :span="2">{{ formatTime(agentDetail.createdAt) }}</el-descriptions-item>
</el-descriptions>
<div class="section-title">{{ t('agent.section.credit_log') }}</div>
<div class="section-title section-title--row">
<span>{{ t('agent.section.credit_log') }}</span>
<el-button
v-if="agentDetail"
link
type="primary"
@click="router.push({ path: '/agent-credit-transactions', query: { agentId: agentDetail.userId } })"
>
{{ t('agent.credit_tx.view_all') }}
</el-button>
</div>
<el-table :data="agentDetail.recentCreditTransactions" size="small" stripe :empty-text="t('agent.col.no_records')">
<el-table-column :label="t('agent.col.credit_type')" width="80">
<template #default="{ row }">{{ creditTypeLabel(row.transactionType) }}</template>
@@ -1627,6 +1637,13 @@ function creditTypeLabel(type: string) {
color: #666;
margin-bottom: 8px;
}
.section-title--row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
</style>
<style>

View File

@@ -1,10 +1,12 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { useAdminLocale } from '../composables/useAdminLocale';
import { resolveFormError, resolveApiError } from '../i18n/form-validation';
import api from '../api';
const { t } = useAdminLocale();
const router = useRouter();
import { ElMessage } from 'element-plus';
import {
emptyAgentCreateForm,
@@ -17,7 +19,7 @@ import {
type AgentCreateForm,
type AgentEditForm,
type PromotableUserOption,
} from './agent-form.ts';
} from './agent-form';
import {
formatAmount,
formatAmountFull,
@@ -484,7 +486,17 @@ function creditTypeLabel(type: string) {
</el-descriptions-item>
</el-descriptions>
<div class="section-title">{{ t('agent.section.credit_log') }}</div>
<div class="section-title section-title--row">
<span>{{ t('agent.section.credit_log') }}</span>
<el-button
v-if="detail"
link
type="primary"
@click="router.push({ path: '/agent-credit-transactions', query: { agentId: detail.userId } })"
>
{{ t('agent.credit_tx.view_all') }}
</el-button>
</div>
<el-table
:data="detail.recentCreditTransactions"
size="small"
@@ -527,6 +539,13 @@ function creditTypeLabel(type: string) {
color: #666;
margin-bottom: 8px;
}
.section-title--row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.amount-compact {
white-space: nowrap;
font-variant-numeric: tabular-nums;

View File

@@ -15,7 +15,7 @@ import AdminTableEmpty from '../components/AdminTableEmpty.vue';
const { t, localeTag } = useAdminLocale();
const { statusOptions, typeOptions } = useBetFilterOptions();
import type { BetListRow, BetDetail } from './bet-form.ts';
import type { BetListRow, BetDetail } from './bet-form';
const bets = ref<BetListRow[]>([]);
const total = ref(0);

View File

@@ -12,7 +12,7 @@ import { getBuiltinCountry } from '../data/builtinCountries';
import {
readMatchesListUiState,
writeMatchesListUiState,
} from '../utils/matchesListState.ts';
} from '../utils/matchesListState';
import { formatAmount } from '../utils/format-amount';
import {
emptyMatchForm,
@@ -20,7 +20,7 @@ import {
fillBuiltinTeam,
clearBuiltinTeam,
type MatchCreateForm,
} from './match-form.ts';
} from './match-form';
const { t } = useAdminLocale();
const route = useRoute();

View File

@@ -8,7 +8,7 @@ import LeagueOutrightOddsPanel from './matches/LeagueOutrightOddsPanel.vue';
import {
readMatchesListUiState,
writeMatchesListUiState,
} from '../utils/matchesListState.ts';
} from '../utils/matchesListState';
const { t } = useAdminLocale();
const route = useRoute();

View File

@@ -15,9 +15,10 @@ import {
betStatusLabel,
betStatusTagType,
betTypeLabel,
betResultLabel,
} from '../utils/bet-labels';
import { adminSelectionLabel } from '../utils/adminSelectionLabel.ts';
import type { AdminMatchDetail } from './match-form.ts';
import { adminSelectionLabel } from '../utils/adminSelectionLabel';
import type { AdminMatchDetail } from './match-form';
import AdminSubNav from '../components/AdminSubNav.vue';
interface SettlementBetStats {

View File

@@ -0,0 +1,403 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import { useAdminLocale } from '../composables/useAdminLocale';
import api from '../api';
import AdminTableEmpty from '../components/AdminTableEmpty.vue';
const { t, locale, localeTag } = useAdminLocale();
type SuiteInfo = { id: string; name: string; description: string; caseCount: number };
type CaseMeta = { id: string; suite: string; name: string; uatRef?: string };
type CaseResult = CaseMeta & {
status: 'PASS' | 'FAIL' | 'SKIP';
durationMs: number;
stepCount?: number;
message?: string;
error?: string;
details?: string[];
};
type RunSummary = {
runId: string;
startedAt: string;
finishedAt: string;
durationMs: number;
total: number;
passed: number;
failed: number;
skipped: number;
suites: string[];
results: CaseResult[];
};
const suites = ref<SuiteInfo[]>([]);
const selectedSuites = ref<string[]>([]);
const running = ref(false);
const lastRun = ref<RunSummary | null>(null);
const filterStatus = ref('');
const suiteName = (id: string) => suites.value.find((s) => s.id === id)?.name ?? id;
const filteredResults = computed(() => {
const rows = lastRun.value?.results ?? [];
if (!filterStatus.value) return rows;
return rows.filter((r) => r.status === filterStatus.value);
});
const passRate = computed(() => {
if (!lastRun.value?.total) return 0;
return Math.round((lastRun.value.passed / lastRun.value.total) * 100);
});
const runLogText = computed(() => (lastRun.value ? formatRunLog(lastRun.value) : ''));
function formatCaseHeader(row: CaseResult) {
const parts = [
`[${row.status}]`,
row.id,
suiteName(row.suite),
row.name,
row.uatRef ? `UAT:${row.uatRef}` : null,
row.stepCount ? `${row.stepCount} steps` : null,
`${row.durationMs}ms`,
row.error || row.message || '',
].filter(Boolean);
return parts.join(' | ');
}
function formatCaseBlock(row: CaseResult) {
const lines = [formatCaseHeader(row)];
if (row.details?.length) {
lines.push(...row.details.map((d) => ` ${d.replace(/\n/g, '\n ')}`));
}
return lines.join('\n');
}
function formatCaseLine(row: CaseResult) {
return formatCaseBlock(row);
}
function formatRunLog(run: RunSummary) {
const lines = [
'=== Smoke Test Report ===',
`Run ID: ${run.runId}`,
`Started: ${run.startedAt}`,
`Finished: ${run.finishedAt}`,
`Duration: ${run.durationMs}ms`,
`Summary: pass=${run.passed}, fail=${run.failed}, skip=${run.skipped}, total=${run.total}`,
`Suites: ${run.suites.join(', ')}`,
'',
'--- Cases ---',
...run.results.map((row) => formatCaseBlock(row)),
];
return lines.join('\n\n');
}
async function copyText(text: string) {
if (!text) return;
try {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
} else {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.left = '-9999px';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
ElMessage.success(t('smoke.msg.copy_ok'));
} catch {
ElMessage.error(t('smoke.msg.copy_failed'));
}
}
async function loadMeta() {
const { data } = await api.get('/admin/smoke-tests/suites');
suites.value = data.data?.suites ?? [];
if (data.data?.lastRun) lastRun.value = data.data.lastRun;
if (!selectedSuites.value.length) {
selectedSuites.value = suites.value.map((s) => s.id);
}
}
async function runTests() {
running.value = true;
try {
const { data } = await api.post('/admin/smoke-tests/run', {
suites: selectedSuites.value.length ? selectedSuites.value : undefined,
});
lastRun.value = data.data as RunSummary;
if (lastRun.value.failed > 0) {
ElMessage.warning(t('smoke.msg.has_failures', { n: lastRun.value.failed }));
} else {
ElMessage.success(t('smoke.msg.all_passed', { n: lastRun.value.passed }));
}
} catch (e: unknown) {
const msg = (e as { response?: { data?: { error?: string } } })?.response?.data?.error;
ElMessage.error(msg || t('smoke.msg.run_failed'));
} finally {
running.value = false;
}
}
function formatTime(v: string) {
return new Date(v).toLocaleString(localeTag.value);
}
function statusTagType(status: string) {
if (status === 'PASS') return 'success';
if (status === 'FAIL') return 'danger';
return 'info';
}
onMounted(loadMeta);
</script>
<template>
<div class="admin-list-page smoke-page">
<el-card class="intro-card" shadow="never">
<p class="intro-text">{{ t('smoke.intro') }}</p>
<ul class="intro-list">
<li>{{ t('smoke.intro_rule') }}</li>
<li>{{ t('smoke.intro_db') }}</li>
<li>{{ t('smoke.intro_bet_flow') }}</li>
<li>{{ t('smoke.intro_note') }}</li>
</ul>
</el-card>
<el-card class="filter-card" shadow="never">
<el-form inline>
<el-form-item :label="t('smoke.field.suites')">
<el-select
v-model="selectedSuites"
multiple
collapse-tags
collapse-tags-tooltip
:placeholder="t('smoke.ph.suites')"
style="min-width: 320px"
>
<el-option
v-for="s in suites"
:key="s.id"
:label="`${s.name} (${s.caseCount})`"
:value="s.id"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="running" @click="runTests">
{{ t('smoke.btn.run') }}
</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card v-if="lastRun" class="data-card summary-card" shadow="never">
<div class="summary-head">
<div>
<div class="summary-title">{{ t('smoke.last_run') }}</div>
<div class="summary-meta">
{{ formatTime(lastRun.startedAt) }} · {{ lastRun.durationMs }}ms · {{ lastRun.runId }}
</div>
</div>
<div class="summary-actions">
<div class="summary-stats">
<el-tag type="success">{{ t('smoke.stat.pass') }} {{ lastRun.passed }}</el-tag>
<el-tag type="danger">{{ t('smoke.stat.fail') }} {{ lastRun.failed }}</el-tag>
<el-tag>{{ t('smoke.stat.total') }} {{ lastRun.total }}</el-tag>
<el-tag type="warning">{{ passRate }}%</el-tag>
</div>
<el-button size="small" @click="copyText(runLogText)">{{ t('smoke.btn.copy_all') }}</el-button>
</div>
</div>
</el-card>
<el-card v-if="lastRun" class="data-card" shadow="never">
<div class="table-head">
<span class="table-title">{{ t('smoke.results_title') }}</span>
<el-select v-model="filterStatus" clearable :placeholder="t('common.all')" style="width: 120px">
<el-option :label="t('smoke.status.PASS')" value="PASS" />
<el-option :label="t('smoke.status.FAIL')" value="FAIL" />
</el-select>
</div>
<div class="table-wrap">
<el-table :key="locale" :data="filteredResults" stripe row-key="id">
<template #empty>
<AdminTableEmpty />
</template>
<el-table-column type="expand" width="44">
<template #default="{ row }">
<div v-if="row.details?.length" class="case-details">
<pre>{{ row.details.join('\n\n') }}</pre>
</div>
<span v-else class="case-details-empty">{{ t('smoke.no_steps') }}</span>
</template>
</el-table-column>
<el-table-column prop="id" :label="t('smoke.col.id')" width="88" />
<el-table-column :label="t('smoke.col.suite')" width="120">
<template #default="{ row }">{{ suiteName(row.suite) }}</template>
</el-table-column>
<el-table-column prop="name" :label="t('smoke.col.name')" min-width="180" />
<el-table-column prop="uatRef" :label="t('smoke.col.uat')" width="88">
<template #default="{ row }">{{ row.uatRef ?? '—' }}</template>
</el-table-column>
<el-table-column :label="t('common.status')" width="96">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" size="small">
{{ t(`smoke.status.${row.status}`) }}
</el-tag>
</template>
</el-table-column>
<el-table-column :label="t('smoke.col.steps')" width="72" align="center">
<template #default="{ row }">{{ row.stepCount ?? '—' }}</template>
</el-table-column>
<el-table-column :label="t('smoke.col.duration')" width="96" align="right">
<template #default="{ row }">{{ row.durationMs }}ms</template>
</el-table-column>
<el-table-column :label="t('smoke.col.message')" min-width="200" show-overflow-tooltip>
<template #default="{ row }">
<span :class="{ 'fail-msg': row.status === 'FAIL' }">{{ row.error || row.message || '—' }}</span>
</template>
</el-table-column>
<el-table-column :label="t('common.actions')" width="88" fixed="right">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="copyText(formatCaseBlock(row))">
{{ t('smoke.btn.copy_one') }}
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="log-panel">
<div class="log-head">
<span class="table-title">{{ t('smoke.log_title') }}</span>
<el-button size="small" @click="copyText(runLogText)">{{ t('smoke.btn.copy_all') }}</el-button>
</div>
<el-input
type="textarea"
class="log-textarea"
:model-value="runLogText"
readonly
:rows="18"
resize="vertical"
/>
</div>
</el-card>
<el-card v-else class="data-card empty-card" shadow="never">
<p>{{ t('smoke.empty') }}</p>
</el-card>
</div>
</template>
<style scoped>
.intro-text {
margin: 0 0 8px;
color: #666;
line-height: 1.5;
}
.intro-list {
margin: 0;
padding-left: 18px;
color: #888;
font-size: 13px;
line-height: 1.6;
}
.summary-head {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 12px;
align-items: flex-start;
}
.summary-title {
font-size: 15px;
font-weight: 700;
}
.summary-meta {
font-size: 12px;
color: #888;
margin-top: 4px;
}
.summary-actions {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8px;
}
.summary-stats {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: flex-end;
}
.log-panel {
margin-top: 16px;
border-top: 1px solid var(--el-border-color-lighter);
padding-top: 12px;
}
.log-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.log-textarea :deep(textarea) {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 12px;
line-height: 1.5;
}
.table-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.table-title {
font-weight: 700;
}
.fail-msg {
color: var(--el-color-danger);
}
.case-details {
padding: 8px 12px 12px 48px;
}
.case-details pre {
margin: 0;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 12px;
line-height: 1.55;
white-space: pre-wrap;
word-break: break-word;
color: #444;
}
.case-details-empty {
padding: 8px 12px 12px 48px;
color: #999;
font-size: 13px;
}
.empty-card {
text-align: center;
color: #888;
padding: 24px 0;
}
</style>

View File

@@ -18,7 +18,7 @@ import {
type PlayerDetail,
type PlayerCreateForm,
type PlayerEditForm,
} from './user-form.ts';
} from './user-form';
import {
formatAmount,
formatAmountFull,

View File

@@ -472,7 +472,7 @@ function openCreditSub(row: AgentSubAgentRow) {
creditContext.value = {
target: snapshotFromAgentRow(row),
parent: snapshotFromAgentRow({
username: auth.user?.username ?? t('credit.context.acting_agent'),
username: auth.user.value?.username ?? t('credit.context.acting_agent'),
creditLimit: String(profile.value.creditLimit ?? 0),
usedCredit: String(profile.value.usedCredit ?? 0),
availableCredit: String(profile.value.availableCredit ?? 0),

View File

@@ -114,7 +114,7 @@ function openCredit(row: AgentSubAgentRow) {
creditContext.value = {
target: snapshotFromAgentRow(row),
parent: snapshotFromAgentRow({
username: auth.user?.username ?? t('credit.context.acting_agent'),
username: auth.user.value?.username ?? t('credit.context.acting_agent'),
creditLimit: String(profile.value.creditLimit ?? 0),
usedCredit: String(profile.value.usedCredit ?? 0),
availableCredit: String(profile.value.availableCredit ?? 0),

View File

@@ -4,7 +4,7 @@ import { useRouter } from 'vue-router';
import { ElMessage, ElMessageBox } from 'element-plus';
import { useAdminLocale } from '../../composables/useAdminLocale';
import api from '../../api';
import { ensureLeagueExpanded } from '../../utils/matchesListState.ts';
import { ensureLeagueExpanded } from '../../utils/matchesListState';
import { formatAmount } from '../../utils/format-amount';
const props = defineProps<{
leagueId: string;

View File

@@ -13,7 +13,7 @@ import {
formFromDetail,
type AdminMatchDetail,
type MatchCreateForm,
} from '../match-form.ts';
} from '../match-form';
import AdminSubNav from '../../components/AdminSubNav.vue';
const route = useRoute();

View File

@@ -5,7 +5,7 @@ import { ElMessage } from 'element-plus';
import { useAdminLocale } from '../../composables/useAdminLocale';
import api from '../../api';
import MatchMarketsPanel from './MatchMarketsPanel.vue';
import type { AdminMatchDetail } from '../match-form.ts';
import type { AdminMatchDetail } from '../match-form';
import AdminSubNav from '../../components/AdminSubNav.vue';
const route = useRoute();

View File

@@ -3,9 +3,9 @@ import { ref, watch } from 'vue';
import { ElMessage } from 'element-plus';
import { useAdminLocale } from '../../composables/useAdminLocale';
import api from '../../api';
import type { AdminMatchDetail } from '../match-form.ts';
import { defaultSelectionName } from '../../utils/selectionDefaults.ts';
import { adminSelectionLabel } from '../../utils/adminSelectionLabel.ts';
import type { AdminMatchDetail } from '../match-form';
import { defaultSelectionName } from '../../utils/selectionDefaults';
import { adminSelectionLabel } from '../../utils/adminSelectionLabel';
const props = defineProps<{
matchId: string;