diff --git a/backend/UniVerse.Api/Controllers/ReviewsController.cs b/backend/UniVerse.Api/Controllers/ReviewsController.cs index 6c4634d..53dc79d 100644 --- a/backend/UniVerse.Api/Controllers/ReviewsController.cs +++ b/backend/UniVerse.Api/Controllers/ReviewsController.cs @@ -43,6 +43,20 @@ public class ReviewsController : ControllerBase public async Task> Create([FromBody] CreateReviewRequest req) => CreatedAtAction(nameof(Get), new { id = 0 }, await _reviews.CreateAsync(CurrentUserId, req)); + /// Получить список всех отзывов. + /// Только Admin. Возвращает все отзывы независимо от LLM-статуса. + /// Параметры пагинации. + /// Список всех отзывов (пагинированный). + /// Требуется аутентификация. + /// Требуется роль Admin. + [Authorize(Roles = "Admin")] + [HttpGet] + [ProducesResponseType(typeof(PagedResult), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task List([FromQuery] PaginationRequest pagination) => + Ok(await _reviews.GetAllAsync(pagination)); + /// Получить отзыв по ID. /// Только Admin или Teacher. /// ID отзыва. diff --git a/backend/UniVerse.Api/openapi.json b/backend/UniVerse.Api/openapi.json index 62c98b1..fd4ef0c 100644 --- a/backend/UniVerse.Api/openapi.json +++ b/backend/UniVerse.Api/openapi.json @@ -2503,6 +2503,68 @@ "Bearer": [ ] } ] + }, + "get": { + "tags": [ + "Reviews" + ], + "summary": "Получить список всех отзывов.", + "description": "Только Admin. Возвращает все отзывы независимо от LLM-статуса.\n\n**Required roles:** Admin", + "parameters": [ + { + "name": "Page", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "PageSize", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Список всех отзывов (пагинированный).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReviewDtoPagedResult" + } + } + } + }, + "401": { + "description": "Требуется аутентификация.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "403": { + "description": "Требуется роль Admin.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + }, + "security": [ + { + "Bearer": [ ] + } + ] } }, "/api/v1/reviews/{id}": { diff --git a/backend/UniVerse.Application/Interfaces/IReviewService.cs b/backend/UniVerse.Application/Interfaces/IReviewService.cs index 8d3330a..003b16d 100644 --- a/backend/UniVerse.Application/Interfaces/IReviewService.cs +++ b/backend/UniVerse.Application/Interfaces/IReviewService.cs @@ -11,6 +11,7 @@ public interface IReviewService Task DeleteAsync(int id, int userId, bool isAdmin = false); Task> GetByLectureAsync(int lectureId, PaginationRequest pagination); Task> GetByUserAsync(int userId, PaginationRequest pagination); + Task> GetAllAsync(PaginationRequest pagination); Task> GetPendingAsync(PaginationRequest pagination); Task ReanalyzeAsync(int id); } diff --git a/backend/UniVerse.Infrastructure/Services/ReviewService.cs b/backend/UniVerse.Infrastructure/Services/ReviewService.cs index b27b776..bc1205a 100644 --- a/backend/UniVerse.Infrastructure/Services/ReviewService.cs +++ b/backend/UniVerse.Infrastructure/Services/ReviewService.cs @@ -86,6 +86,15 @@ public class ReviewService : IReviewService return PagedResult.Create(items.Select(r => r.ToDto()).ToList(), total, pagination.Page, pagination.PageSize); } + public async Task> GetAllAsync(PaginationRequest pagination) + { + var query = BaseQuery(); + var total = await query.CountAsync(); + var items = await query.OrderByDescending(r => r.CreatedAt) + .Skip((pagination.Page - 1) * pagination.PageSize).Take(pagination.PageSize).ToListAsync(); + return PagedResult.Create(items.Select(r => r.ToDto()).ToList(), total, pagination.Page, pagination.PageSize); + } + public async Task> GetPendingAsync(PaginationRequest pagination) { var query = BaseQuery().Where(r => r.LlmStatus == ReviewLlmStatus.Pending); diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index afcee0f..013b28e 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -121,6 +121,10 @@ export const reviewsApi = { method: 'POST', body: JSON.stringify({ lectureId: Number(lectureId), rating, text }), }), + async list(query: Record = { PageSize: 100 }) { + const payload = await apiRequest | ReviewDto[]>('/reviews', { query }) + return extractItems(payload) + }, async pending() { const payload = await apiRequest | ReviewDto[]>('/reviews/pending') return extractItems(payload) diff --git a/frontend/src/components/layout/AppSidebar.vue b/frontend/src/components/layout/AppSidebar.vue index 23502aa..d1aa4b4 100644 --- a/frontend/src/components/layout/AppSidebar.vue +++ b/frontend/src/components/layout/AppSidebar.vue @@ -26,6 +26,7 @@ const navItems: NavItem[] = [ { label: 'Пользователи', icon: 'users', to: '/admin/users', roles: ['admin'] }, { label: 'Лекции', icon: 'books', to: '/admin/lectures', roles: ['admin'] }, { label: 'ИИ очередь', icon: 'robot', to: '/admin/llm-queue', roles: ['admin'] }, + { label: 'Отзывы', icon: 'message-circle', to: '/admin/reviews', roles: ['admin'] }, ] const visible = computed(() => diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 72a641e..88c551c 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -32,6 +32,7 @@ const router = createRouter({ { path: '/admin/users', name: 'admin-users', component: () => import('@/views/admin/AdminUsersView.vue'), meta: { role: 'admin' } }, { path: '/admin/lectures', name: 'admin-lectures', component: () => import('@/views/admin/AdminLecturesView.vue'), meta: { role: 'admin' } }, { path: '/admin/llm-queue', name: 'admin-llm', component: () => import('@/views/admin/AdminLLMQueueView.vue'), meta: { role: 'admin' } }, + { path: '/admin/reviews', name: 'admin-reviews', component: () => import('@/views/admin/AdminReviewsView.vue'), meta: { role: 'admin' } }, { path: '/:pathMatch(.*)*', redirect: '/' }, ], diff --git a/frontend/src/views/admin/AdminReviewsView.vue b/frontend/src/views/admin/AdminReviewsView.vue new file mode 100644 index 0000000..807c003 --- /dev/null +++ b/frontend/src/views/admin/AdminReviewsView.vue @@ -0,0 +1,222 @@ + + + + + diff --git a/frontend/src/views/student/ReviewFormView.vue b/frontend/src/views/student/ReviewFormView.vue index b064056..a6dcb7d 100644 --- a/frontend/src/views/student/ReviewFormView.vue +++ b/frontend/src/views/student/ReviewFormView.vue @@ -2,6 +2,7 @@ import { ref } from 'vue' import { useRoute } from 'vue-router' import GlassCard from '@/components/ui/GlassCard.vue' +import AppIcon from '@/components/ui/AppIcon.vue' import { reviewsApi } from '@/api' const route = useRoute() @@ -44,7 +45,7 @@ async function submit() {
-
+
Отзыв отправлен и будет обработан
Спасибо! Оценка полезности отзыва рассчитывается автоматически. Студентам не показывается техническая оценка LLM. @@ -107,7 +108,7 @@ textarea { .hint { font-size: 12px; color: var(--color-text-secondary); background: rgba(255,255,255,0.6); padding: 8px 12px; border-radius: var(--radius-sm); } .form-actions { display: flex; gap: 10px; } .success-state { display: flex; flex-direction: column; gap: 10px; align-items: flex-start; } -.success-icon { font-size: 28px; } +.success-icon { color: var(--color-primary); } .success-title { font-size: 16px; font-weight: 700; } .success-sub { font-size: 13px; color: var(--color-text-secondary); } .error { color: var(--color-error); font-size: 13px; }