feat: добавил личные уведомления для пользователей
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Successful in 9s
🚀 Create and publish a Docker image / Build & publish backend image (push) Successful in 26s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Successful in 19s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Successful in 8s

Реализовал хранение, получение и отметку прочитанными пользовательских уведомлений. Обновил фронтенд для отображения и управления уведомлениями в профиле студента.
This commit is contained in:
2026-05-12 23:54:55 +03:00
parent feff77b232
commit b0a4a6d259
19 changed files with 401 additions and 10 deletions
+9
View File
@@ -16,6 +16,7 @@ import type {
UserAchievementDto,
UserDto,
UserQuery,
UserNotificationDto,
UserStatsDto,
} from './types'
@@ -85,6 +86,14 @@ export const achievementsApi = {
},
}
export const notificationsApi = {
async list() {
const payload = await apiRequest<PagedResult<UserNotificationDto> | UserNotificationDto[]>('/notifications')
return extractItems(payload)
},
markAllRead: () => apiRequest<void>('/notifications/read-all', { method: 'PATCH' }),
}
export const reviewsApi = {
create: (lectureId: string | number, rating: 'Like' | 'Neutral' | 'Dislike', text: string) =>
apiRequest<ReviewDto>('/reviews', {
+13 -1
View File
@@ -1,4 +1,4 @@
import type { Achievement, CoinTransaction, Lecture, Review, User, UserRole } from '@/types'
import type { Achievement, CoinTransaction, Lecture, Notification, Review, User, UserRole } from '@/types'
import type {
AchievementDto,
CoinTransactionDto,
@@ -8,6 +8,7 @@ import type {
UserDto,
UserStatsDto,
UserAchievementDto,
UserNotificationDto,
} from './types'
export function mapApiRole(role: string | undefined): UserRole {
@@ -127,3 +128,14 @@ export function mapApiCoinTransaction(transaction: CoinTransactionDto): CoinTran
type: transaction.amount >= 0 ? 'earned' : 'spent',
}
}
export function mapApiNotification(notification: UserNotificationDto): Notification {
return {
id: String(notification.id),
type: notification.type,
title: notification.title,
body: notification.body,
read: notification.isRead,
createdAt: notification.createdAt,
}
}
+9
View File
@@ -185,6 +185,15 @@ export interface CoinTransactionDto {
createdAt: string
}
export interface UserNotificationDto {
id: number
type: 'reminder' | 'schedule-change' | 'achievement' | 'coins' | 'recommendation'
title: string
body: string
isRead: boolean
createdAt: string
}
export interface LectureQuery {
DateFrom?: string
DateTo?: string
+15 -4
View File
@@ -1,7 +1,7 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { achievementsApi, usersApi } from '@/api'
import { mapApiAchievement, mapApiCoinTransaction } from '@/api/mappers'
import { achievementsApi, notificationsApi, usersApi } from '@/api'
import { mapApiAchievement, mapApiCoinTransaction, mapApiNotification } from '@/api/mappers'
import type { Achievement, CoinTransaction, Notification } from '@/types'
import { useAuthStore } from './auth'
@@ -25,7 +25,10 @@ export const useUserStore = defineStore('user', () => {
usersApi.achievements(id),
usersApi.transactions(id),
])
const achievementCatalog = await achievementsApi.list()
const [achievementCatalog, notificationPayload] = await Promise.all([
achievementsApi.list(),
notificationsApi.list(),
])
if (auth.user) {
auth.setUser({
@@ -55,6 +58,7 @@ export const useUserStore = defineStore('user', () => {
(a, b) => Number(a.id) - Number(b.id),
)
coinHistory.value = transactions.map(mapApiCoinTransaction)
notifications.value = notificationPayload.map(mapApiNotification)
} catch (err) {
error.value = err instanceof Error ? err.message : 'Не удалось загрузить данные профиля.'
} finally {
@@ -62,7 +66,13 @@ export const useUserStore = defineStore('user', () => {
}
}
function markAllRead() {
async function fetchNotifications() {
const payload = await notificationsApi.list()
notifications.value = payload.map(mapApiNotification)
}
async function markAllRead() {
await notificationsApi.markAllRead()
notifications.value.forEach(n => (n.read = true))
}
@@ -75,6 +85,7 @@ export const useUserStore = defineStore('user', () => {
loading,
error,
fetchStudentData,
fetchNotifications,
markAllRead,
unreadCount,
}
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed } from 'vue'
import { computed, onMounted } from 'vue'
import { useUserStore } from '@/stores/user'
import GlassCard from '@/components/ui/GlassCard.vue'
import AppIcon from '@/components/ui/AppIcon.vue'
@@ -7,6 +7,10 @@ import EmptyState from '@/components/ui/EmptyState.vue'
const userStore = useUserStore()
onMounted(() => {
void userStore.fetchNotifications()
})
const grouped = computed(() => {
const map: Record<string, typeof userStore.notifications> = {}
userStore.notifications.forEach(n => {