a8d51df3f1
Backend CI / build-and-test (push) Successful in 40s
Frontend CI / build-and-check (push) Failing after 19s
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Successful in 6s
🚀 Create and publish a Docker image / Build & publish backend image (push) Successful in 24s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Successful in 27s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Successful in 5s
85 lines
2.6 KiB
Vue
85 lines
2.6 KiB
Vue
<script setup lang="ts">
|
|
import { computed, onMounted, ref } from 'vue'
|
|
import GlassCard from '@/components/ui/GlassCard.vue'
|
|
import StatsWidget from '@/components/ui/StatsWidget.vue'
|
|
import StatusBadge from '@/components/ui/StatusBadge.vue'
|
|
import { syncApi, usersApi } from '@/api'
|
|
import type { AdminDashboardStatsDto, SyncStatusDto } from '@/api/types'
|
|
|
|
const stats = ref<AdminDashboardStatsDto | null>(null)
|
|
const syncStatus = ref<SyncStatusDto | null>(null)
|
|
const syncMeta = computed(() =>
|
|
syncStatus.value?.lastSyncAt
|
|
? `Последняя синхронизация: ${new Date(syncStatus.value.lastSyncAt).toLocaleString('ru-RU')}`
|
|
: 'Синхронизация ещё не выполнялась',
|
|
)
|
|
|
|
onMounted(async () => {
|
|
const [statsResult, syncResult] = await Promise.allSettled([usersApi.adminStats(), syncApi.status()])
|
|
if (statsResult.status === 'fulfilled') stats.value = statsResult.value
|
|
if (syncResult.status === 'fulfilled') syncStatus.value = syncResult.value
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="admin-dashboard page-content">
|
|
<h1 class="page-title">Дашборд администратора</h1>
|
|
|
|
<div class="stats-row">
|
|
<StatsWidget label="Пользователей" :value="stats?.usersCount ?? 0" icon="users" color="green" />
|
|
<StatsWidget label="Лекций" :value="stats?.lecturesCount ?? 0" icon="books" color="aqua" />
|
|
<StatsWidget
|
|
label="Записей"
|
|
:value="stats?.enrollmentsCount ?? 0"
|
|
icon="calendar-event"
|
|
color="orange"
|
|
/>
|
|
<StatsWidget
|
|
label="Отзывы на проверке"
|
|
:value="stats?.pendingReviewsCount ?? 0"
|
|
icon="message-circle"
|
|
color="purple"
|
|
/>
|
|
</div>
|
|
|
|
<div class="grid">
|
|
<GlassCard>
|
|
<div class="section-title">Состояние синхронизации расписания</div>
|
|
<StatusBadge :status="syncStatus?.status ?? 'pending'" />
|
|
<div class="sync-meta">{{ syncMeta }}</div>
|
|
<div class="sync-error" v-if="syncStatus?.lastResult?.error">
|
|
Ошибка: {{ syncStatus.lastResult.error }}
|
|
</div>
|
|
</GlassCard>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.admin-dashboard {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 18px;
|
|
}
|
|
.stats-row {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
gap: 16px;
|
|
}
|
|
.grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
|
gap: 16px;
|
|
}
|
|
.sync-meta {
|
|
font-size: 12px;
|
|
color: var(--color-text-secondary);
|
|
margin-top: 6px;
|
|
}
|
|
.sync-error {
|
|
font-size: 12px;
|
|
color: var(--color-error);
|
|
margin-top: 8px;
|
|
}
|
|
</style>
|