fix: синхронизации аудиторий
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { coursesApi, lecturesApi, locationsApi, tagsApi } from '@/api'
|
||||
import type { CourseDto, LectureDto, LocationDto, TagDto } from '@/api/types'
|
||||
import { computed, inject, onMounted, ref } from 'vue'
|
||||
import { coursesApi, lecturesApi, locationsApi, syncApi, tagsApi } from '@/api'
|
||||
import type { CourseDto, LectureDto, LocationDto, SyncResultDto, SyncStatusDto, TagDto } from '@/api/types'
|
||||
import GlassCard from '@/components/ui/GlassCard.vue'
|
||||
import DataTable from '@/components/ui/DataTable.vue'
|
||||
import EmptyState from '@/components/ui/EmptyState.vue'
|
||||
@@ -19,6 +19,35 @@ const courses = ref<CourseDto[]>([])
|
||||
const locations = ref<LocationDto[]>([])
|
||||
const tags = ref<TagDto[]>([])
|
||||
const loading = ref(false)
|
||||
const syncingSchedule = ref(false)
|
||||
const syncingRooms = ref(false)
|
||||
const syncError = ref('')
|
||||
const syncStatus = ref<SyncStatusDto | null>(null)
|
||||
const syncResult = ref<SyncResultDto | null>(null)
|
||||
const addToast = inject('addToast') as ((message: string, type?: 'success' | 'error' | 'info') => void) | undefined
|
||||
|
||||
function toInputDateTime(date: Date) {
|
||||
const offsetMs = date.getTimezoneOffset() * 60000
|
||||
return new Date(date.getTime() - offsetMs).toISOString().slice(0, 16)
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
const inTwoWeeks = new Date(now)
|
||||
inTwoWeeks.setDate(inTwoWeeks.getDate() + 14)
|
||||
|
||||
const syncForm = ref({
|
||||
specialtyCode: '',
|
||||
typeId: '',
|
||||
timeMin: toInputDateTime(now),
|
||||
timeMax: toInputDateTime(inTwoWeeks),
|
||||
})
|
||||
|
||||
const syncMeta = computed(() => {
|
||||
if (!syncStatus.value?.lastSyncAt) return 'Синхронизация ещё не выполнялась'
|
||||
return `Последняя: ${new Date(syncStatus.value.lastSyncAt).toLocaleString('ru-RU')}`
|
||||
})
|
||||
|
||||
const visibleSyncResult = computed(() => syncResult.value ?? syncStatus.value?.lastResult ?? null)
|
||||
|
||||
const tabConfig: Record<TabKey, TabConfig> = {
|
||||
lectures: {
|
||||
@@ -107,19 +136,78 @@ const current = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
const [lecturesResult, coursesResult, locationsResult, tagsResult] = await Promise.allSettled([
|
||||
const [lecturesResult, coursesResult, locationsResult, tagsResult, syncStatusResult] = await Promise.allSettled([
|
||||
lecturesApi.list({ PageSize: 100 }),
|
||||
coursesApi.list(),
|
||||
locationsApi.list(),
|
||||
tagsApi.list(),
|
||||
syncApi.status(),
|
||||
])
|
||||
if (lecturesResult.status === 'fulfilled') lectures.value = lecturesResult.value
|
||||
if (coursesResult.status === 'fulfilled') courses.value = coursesResult.value
|
||||
if (locationsResult.status === 'fulfilled') locations.value = locationsResult.value
|
||||
if (tagsResult.status === 'fulfilled') tags.value = tagsResult.value
|
||||
if (syncStatusResult.status === 'fulfilled') syncStatus.value = syncStatusResult.value
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
async function refreshSyncStatus() {
|
||||
try {
|
||||
syncStatus.value = await syncApi.status()
|
||||
} catch {
|
||||
// The table refresh is more important than the status badge here.
|
||||
}
|
||||
}
|
||||
|
||||
async function runScheduleSync() {
|
||||
syncingSchedule.value = true
|
||||
syncError.value = ''
|
||||
syncResult.value = null
|
||||
try {
|
||||
syncResult.value = await syncApi.schedule({
|
||||
specialtyCode: syncForm.value.specialtyCode.trim() || null,
|
||||
typeId: syncForm.value.typeId.trim() || null,
|
||||
timeMin: syncForm.value.timeMin ? new Date(syncForm.value.timeMin).toISOString() : null,
|
||||
timeMax: syncForm.value.timeMax ? new Date(syncForm.value.timeMax).toISOString() : null,
|
||||
})
|
||||
|
||||
if (syncResult.value.error) {
|
||||
syncError.value = syncResult.value.error
|
||||
addToast?.('Синхронизация завершилась с ошибкой.', 'error')
|
||||
} else {
|
||||
addToast?.('Расписание синхронизировано.', 'success')
|
||||
}
|
||||
|
||||
await loadData()
|
||||
} catch (err) {
|
||||
syncError.value = err instanceof Error ? err.message : 'Не удалось синхронизировать расписание.'
|
||||
addToast?.(syncError.value, 'error')
|
||||
await refreshSyncStatus()
|
||||
} finally {
|
||||
syncingSchedule.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function runRoomsSync() {
|
||||
syncingRooms.value = true
|
||||
syncError.value = ''
|
||||
syncResult.value = null
|
||||
try {
|
||||
syncResult.value = await syncApi.rooms()
|
||||
addToast?.('Аудитории синхронизированы.', 'success')
|
||||
await loadData()
|
||||
} catch (err) {
|
||||
syncError.value = err instanceof Error ? err.message : 'Не удалось синхронизировать аудитории.'
|
||||
addToast?.(syncError.value, 'error')
|
||||
} finally {
|
||||
syncingRooms.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -127,7 +215,7 @@ onMounted(async () => {
|
||||
<div class="admin-lectures page-content">
|
||||
<div class="header">
|
||||
<h1 class="page-title">Управление лекциями и справочниками</h1>
|
||||
<button class="btn-primary">Создать запись</button>
|
||||
<button class="btn-secondary" type="button" :disabled="loading" @click="loadData">Обновить</button>
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
@@ -145,21 +233,40 @@ onMounted(async () => {
|
||||
</GlassCard>
|
||||
|
||||
<GlassCard>
|
||||
<div class="section-title">Создать / редактировать</div>
|
||||
<form class="form">
|
||||
<label>Название</label>
|
||||
<input class="glass-input" placeholder="Введите название" />
|
||||
<label>Описание</label>
|
||||
<textarea rows="4" placeholder="Описание записи"></textarea>
|
||||
<label>Статус синхронизации</label>
|
||||
<select class="glass-input">
|
||||
<option>Синхронизировано</option>
|
||||
<option>Ожидает</option>
|
||||
<option>Ошибка</option>
|
||||
</select>
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<div class="section-title">Синхронизация расписания</div>
|
||||
<div class="sync-meta">{{ syncMeta }}</div>
|
||||
</div>
|
||||
<span class="sync-status" :class="syncStatus?.status ?? 'idle'">{{ syncStatus?.status ?? 'idle' }}</span>
|
||||
</div>
|
||||
|
||||
<form class="form" @submit.prevent="runScheduleSync">
|
||||
<label>Период с</label>
|
||||
<input v-model="syncForm.timeMin" class="glass-input" type="datetime-local" />
|
||||
<label>Период по</label>
|
||||
<input v-model="syncForm.timeMax" class="glass-input" type="datetime-local" />
|
||||
<label>Код специальности</label>
|
||||
<input v-model="syncForm.specialtyCode" class="glass-input" placeholder="Например, 09.03.04" />
|
||||
<label>Тип занятия</label>
|
||||
<input v-model="syncForm.typeId" class="glass-input" placeholder="Оставьте пустым для всех типов" />
|
||||
|
||||
<div v-if="visibleSyncResult" class="sync-result">
|
||||
Создано: {{ visibleSyncResult.created }},
|
||||
обновлено: {{ visibleSyncResult.updated }},
|
||||
пропущено: {{ visibleSyncResult.skipped }}
|
||||
</div>
|
||||
<div v-if="syncError || visibleSyncResult?.error" class="sync-error">
|
||||
{{ syncError || visibleSyncResult?.error }}
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn-primary" type="button">Сохранить</button>
|
||||
<button class="btn-secondary" type="button">Отменить</button>
|
||||
<button class="btn-primary" type="submit" :disabled="syncingSchedule">
|
||||
{{ syncingSchedule ? 'Синхронизируем...' : 'Синхронизировать лекции' }}
|
||||
</button>
|
||||
<button class="btn-secondary" type="button" :disabled="syncingRooms" @click="runRoomsSync">
|
||||
{{ syncingRooms ? 'Синхронизируем...' : 'Синхронизировать аудитории' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</GlassCard>
|
||||
@@ -174,7 +281,22 @@ onMounted(async () => {
|
||||
.tabs button { background: rgba(255,255,255,0.7); border: none; padding: 8px 18px; font-size: 13px; cursor: pointer; color: var(--color-text-secondary); }
|
||||
.tabs button.active { background: rgba(34,197,94,0.18); color: var(--color-primary-dark); font-weight: 600; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; }
|
||||
.section-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 10px; }
|
||||
.form { display: flex; flex-direction: column; gap: 10px; }
|
||||
textarea { padding: 10px; border-radius: var(--radius-sm); border: 1px solid var(--color-border-glass); background: rgba(255,255,255,0.8); }
|
||||
.form-actions { display: flex; gap: 10px; }
|
||||
.form label { font-size: 12px; font-weight: 600; color: var(--color-text-secondary); }
|
||||
.form-actions { display: flex; gap: 10px; flex-wrap: wrap; }
|
||||
.sync-meta { font-size: 12px; color: var(--color-text-secondary); margin-top: 4px; }
|
||||
.sync-result { font-size: 13px; color: var(--color-text-secondary); }
|
||||
.sync-error { font-size: 13px; color: var(--color-error); }
|
||||
.sync-status {
|
||||
flex: 0 0 auto;
|
||||
border: 1px solid var(--color-border-glass);
|
||||
border-radius: 999px;
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
background: rgba(255,255,255,0.72);
|
||||
}
|
||||
.sync-status.completed { color: #166534; background: rgba(220,252,231,0.9); border-color: #86EFAC; }
|
||||
.sync-status.failed { color: #991B1B; background: rgba(254,226,226,0.9); border-color: #FCA5A5; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user