Files
dating-app-frontend/src/views/admin/ReportsView.vue
Oscar 10d696f4ca refactor(composables): migrate stores →
composables, align with updated API

  - Replace deleted Pinia stores with
  module-level singleton composables
    (useAuth, useChat, useFeed, useUi) — all
  return reactive({...}) so
    Refs auto-unwrap in both templates and
  script code

  - Align entire codebase with new
  swagger-generated api.ts types:
    · TagDto.value  (was .name) — FeedCard,
  FeedFilters, ProfileEditor,
      ProfileSetupView, MyProfileView,
  ProfileDetailView, useUi
    · MediaItemDto[] / .path  (was mediaUrls[],
  avatarUrl) — FeedCard,
      FeedView, MyProfileView,
  ProfileDetailView
    · ChatDto.status 'active'|'closed'  (was
  isActive: boolean) —
      ChatRoomView, ChatsListView
    · MessageDto.profileId  (was senderId) —
  ChatRoomView, ChatBubble
    · MeResponseDto → fetchMe now calls /me +
  /profiles/my in parallel
    · Token refresh: res.data.data.accessToken
  (nested wrapper) —
      router/index.ts aligned with client.ts
  interceptor

  - Fix FeedCard, ChatBubble imports pointing
  to deleted store files
  - Fix ProfileSetupView form type to avoid
  string|undefined on v-model
  - Fix history.back() → window.history.back()
  via goBack() helper
  - Fix chat.unreadCount possibly-undefined
  guard in ChatsListView
  - Fix MapPicker Leaflet icon cast (as unknown
  as Record<string, unknown>)
2026-06-08 15:01:54 +03:00

207 lines
5.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useUi } from '@/composables/useUi';
import { apiClient } from '@/api/client';
import AppButton from '@/components/common/AppButton.vue';
import LoadingSpinner from '@/components/common/LoadingSpinner.vue';
import EmptyState from '@/components/common/EmptyState.vue';
interface Report {
id: string;
sourceProfileId: string;
entityId: string;
entityType: 'profile' | 'message';
description?: string;
createdAt: string;
resolved?: boolean;
reporterName?: string;
}
const uiStore = useUi();
const reports = ref<Report[]>([]);
const loading = ref(false);
onMounted(async () => {
loading.value = true;
try {
const res = await apiClient.api.reportsControllerGetAll() as unknown as Report[];
reports.value = res;
} catch {
uiStore.addToast('Не удалось загрузить жалобы', 'error');
} finally {
loading.value = false;
}
});
async function banUser(userId: string) {
try {
await apiClient.api.usersControllerBan(userId);
uiStore.addToast('Пользователь заблокирован', 'success');
} catch {
uiStore.addToast('Не удалось заблокировать пользователя', 'error');
}
}
function formatDate(iso: string) {
return new Date(iso).toLocaleDateString('ru', { day: 'numeric', month: 'short', year: 'numeric' });
}
</script>
<template>
<div class="reports-admin">
<header class="reports-admin__header">
<h1 class="reports-admin__title">Жалобы</h1>
<span class="meta">{{ reports.length }} всего</span>
</header>
<div v-if="loading" class="reports-admin__loading">
<LoadingSpinner size="lg" />
</div>
<EmptyState
v-else-if="reports.length === 0"
title="Нет жалоб"
description="Жалобы от пользователей появятся здесь"
icon="default"
/>
<div v-else class="reports-admin__table-wrap">
<table class="reports-table">
<thead>
<tr>
<th>Дата</th>
<th>Тип</th>
<th>Объект</th>
<th>Описание</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
<tr v-for="report in reports" :key="report.id" :class="{ 'reports-table__row--resolved': report.resolved }">
<td class="reports-table__date meta">{{ formatDate(report.createdAt) }}</td>
<td>
<span class="reports-table__type" :class="`reports-table__type--${report.entityType}`">
{{ report.entityType === 'profile' ? 'Профиль' : 'Сообщение' }}
</span>
</td>
<td class="reports-table__entity">
<RouterLink
v-if="report.entityType === 'profile'"
:to="`/profile/${report.entityId}`"
class="reports-table__link"
>Открыть</RouterLink>
<span v-else class="meta">{{ report.entityId.slice(0, 8) }}</span>
</td>
<td class="reports-table__desc">{{ report.description ?? '—' }}</td>
<td>
<AppButton
v-if="report.entityType === 'profile'"
variant="danger"
size="sm"
@click="banUser(report.sourceProfileId)"
>Заблокировать</AppButton>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<style scoped lang="scss">
.reports-admin {
height: 100%;
display: flex;
flex-direction: column;
&__header {
display: flex;
align-items: baseline;
gap: 12px;
padding: 20px 24px 16px;
border-bottom: 1px solid var(--color-border);
flex-shrink: 0;
}
&__title {
font-family: var(--font-display);
font-size: 1.5rem;
color: var(--color-cream);
margin: 0;
}
&__loading {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
&__table-wrap {
flex: 1;
overflow: auto;
padding: 16px 24px;
}
}
.reports-table {
width: 100%;
border-collapse: collapse;
font-family: var(--font-mono);
font-size: 0.8125rem;
th {
text-align: left;
font-weight: 500;
letter-spacing: 0.06em;
text-transform: uppercase;
font-size: 0.625rem;
color: var(--color-muted);
padding: 8px 12px;
border-bottom: 1px solid var(--color-border);
}
td {
padding: 10px 12px;
color: var(--color-cream);
border-bottom: 1px solid var(--color-border);
vertical-align: middle;
}
tr:last-child td { border-bottom: none; }
&__row--resolved td { opacity: 0.5; }
&__date { color: var(--color-muted) !important; font-variant-numeric: tabular-nums; }
&__type {
padding: 2px 8px;
border-radius: var(--radius-full);
font-size: 0.625rem;
font-weight: 500;
letter-spacing: 0.06em;
text-transform: uppercase;
&--profile { background: var(--color-signal-bg); color: var(--color-signal); }
&--message { background: rgba(240, 235, 224, 0.06); color: var(--color-muted); }
}
&__link {
color: var(--color-cream);
text-decoration: underline;
text-underline-offset: 3px;
text-decoration-color: var(--color-border);
transition: color var(--transition-fast);
&:hover { color: var(--color-signal); }
}
&__desc {
max-width: 280px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--color-muted) !important;
}
}
</style>