Files
ospab.host/ospabhost/frontend/src/services/notificationService.ts
2025-11-23 14:35:16 +03:00

223 lines
7.4 KiB
TypeScript

import apiClient from '../utils/apiClient';
const API_URL = '/api/notifications';
// Тип для уведомления
export interface Notification {
id: number;
userId: number;
type: string;
title: string;
message: string;
serverId?: number;
ticketId?: number;
checkId?: number;
actionUrl?: string;
icon?: string;
color?: string;
isRead: boolean;
createdAt: string;
}
// Тип для PushSubscription
export interface PushSubscriptionData {
endpoint: string;
keys: {
p256dh: string;
auth: string;
};
}
// Получить токен из localStorage
const getAuthHeader = () => {
const token = localStorage.getItem('access_token');
return token ? { Authorization: `Bearer ${token}` } : {};
};
// Получить список уведомлений
export const getNotifications = async (params?: {
page?: number;
limit?: number;
unreadOnly?: boolean;
type?: string;
}): Promise<{ notifications: Notification[]; total: number; unreadCount: number }> => {
try {
const response = await apiClient.get(API_URL, {
headers: getAuthHeader(),
params
});
// Проверяем структуру ответа
if (response.data && response.data.success !== false) {
// Адаптируем формат ответа от сервера
const data = response.data;
return {
notifications: data.data || [],
total: data.pagination?.total || 0,
unreadCount: data.unreadCount || 0
};
} else {
console.error('Ошибка ответа:', response.data);
throw new Error(response.data?.message || 'Ошибка сервера');
}
} catch (error) {
console.error('Ошибка загрузки уведомлений:', error);
// Возвращаем пустой объект вместо выброса ошибки
return { notifications: [], total: 0, unreadCount: 0 };
}
};
// Получить количество непрочитанных
export const getUnreadCount = async (): Promise<number> => {
try {
const response = await apiClient.get(`${API_URL}/unread-count`, {
headers: getAuthHeader()
});
return response.data.count || 0;
} catch (error) {
console.error('Ошибка получения счетчика:', error);
return 0;
}
};
// Пометить уведомление как прочитанное
export const markAsRead = async (id: number): Promise<void> => {
await apiClient.post(`${API_URL}/${id}/read`, {}, {
headers: getAuthHeader()
});
};
// Пометить все как прочитанные
export const markAllAsRead = async (): Promise<void> => {
await apiClient.post(`${API_URL}/read-all`, {}, {
headers: getAuthHeader()
});
};
// Удалить уведомление
export const deleteNotification = async (id: number): Promise<void> => {
await apiClient.delete(`${API_URL}/${id}`, {
headers: getAuthHeader()
});
};
// Удалить все прочитанные
export const deleteAllRead = async (): Promise<void> => {
await apiClient.delete(`${API_URL}/read/all`, {
headers: getAuthHeader()
});
};
// Получить публичный VAPID ключ
export const getVapidKey = async (): Promise<string> => {
const response = await apiClient.get(`${API_URL}/vapid-key`, {
headers: getAuthHeader()
});
return response.data.publicKey;
};
// Подписаться на Push-уведомления
export const subscribePush = async (subscription: PushSubscriptionData): Promise<void> => {
await apiClient.post(`${API_URL}/subscribe-push`, { subscription }, {
headers: getAuthHeader()
});
};
// Отписаться от Push-уведомлений
export const unsubscribePush = async (endpoint: string): Promise<void> => {
await apiClient.delete(`${API_URL}/unsubscribe-push`, {
headers: getAuthHeader(),
data: { endpoint }
});
};
// Запросить разрешение на Push-уведомления и зарегистрировать Service Worker
export const requestPushPermission = async (): Promise<boolean> => {
if (!('Notification' in window)) {
console.error('❌ Браузер не поддерживает уведомления');
return false;
}
if (!('serviceWorker' in navigator)) {
console.error('❌ Браузер не поддерживает Service Worker');
return false;
}
// Запрос разрешения
console.log('📝 Запрашиваем разрешение на уведомления...');
const permission = await Notification.requestPermission();
console.log('📝 Результат запроса разрешения:', permission);
if (permission !== 'granted') {
console.log('❌ Пользователь отклонил разрешение на уведомления');
return false;
}
try {
// Регистрация Service Worker
console.log('📝 Регистрируем Service Worker...');
const registration = await navigator.serviceWorker.register('/service-worker.js');
console.log('✅ Service Worker зарегистрирован:', registration);
console.log('📝 Ожидаем готовности Service Worker...');
await navigator.serviceWorker.ready;
console.log('✅ Service Worker готов');
// Получаем публичный VAPID ключ
console.log('📝 Получаем VAPID ключ...');
const vapidPublicKey = await getVapidKey();
console.log('✅ VAPID ключ получен:', vapidPublicKey.substring(0, 20) + '...');
// Создаём подписку
console.log('📝 Создаём Push подписку...');
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey) as BufferSource
});
console.log('✅ Push подписка создана:', subscription.endpoint);
// Отправляем подписку на сервер
console.log('📝 Отправляем подписку на сервер...');
const subscriptionData: PushSubscriptionData = {
endpoint: subscription.endpoint,
keys: {
p256dh: arrayBufferToBase64(subscription.getKey('p256dh')!),
auth: arrayBufferToBase64(subscription.getKey('auth')!)
}
};
await subscribePush(subscriptionData);
console.log('✅ Push-уведомления успешно подключены');
return true;
} catch (error) {
console.error('❌ Ошибка подключения Push-уведомлений:', error);
return false;
}
};
// Утилита: конвертация base64 VAPID ключа в Uint8Array
function urlBase64ToUint8Array(base64String: string): Uint8Array {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
// Утилита: конвертация ArrayBuffer в base64
function arrayBufferToBase64(buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}