Files
qlockify-frontend-deployment/src/api/notifications.ts

173 lines
4.7 KiB
TypeScript

import { API_BASE_URL } from "../config/constants"
import { authFetch } from "./client"
export type NotificationLevel = "info" | "success" | "warning" | "error"
export interface NotificationItem {
id: string
type: string
title: string
message: string
level: NotificationLevel
created_at: string
is_seen: boolean
delete_on_seen: boolean
action_url?: string | null
entity_type?: string | null
entity_id?: string | null
meta?: Record<string, unknown>
}
export interface NotificationsResponse {
count: number
unread_count: number
notifications: NotificationItem[]
}
export interface NotificationStreamTokenResponse {
token: string
expires_in: number
}
export interface NotificationFilters {
limit?: number
offset?: number
type?: string
}
const buildSearchParams = (filters: NotificationFilters = {}) => {
const searchParams = new URLSearchParams()
if (typeof filters.limit === "number") {
searchParams.set("limit", String(filters.limit))
}
if (typeof filters.offset === "number") {
searchParams.set("offset", String(filters.offset))
}
if (filters.type) {
searchParams.set("type", filters.type)
}
const query = searchParams.toString()
return query ? `?${query}` : ""
}
export const getNotifications = async (
filters: NotificationFilters = {},
): Promise<NotificationsResponse> => {
const response = await authFetch(`/api/notifications/list/${buildSearchParams(filters)}`)
if (!response.ok) {
throw new Error("Failed to load notifications")
}
return response.json()
}
export const markNotificationSeen = async (id: string) => {
const response = await authFetch("/api/notifications/seen/", {
method: "POST",
body: JSON.stringify({ id }),
})
if (!response.ok) {
throw new Error("Failed to mark notification as read")
}
return response.json()
}
export const deleteNotification = async (id: string) => {
const response = await authFetch(`/api/notifications/${id}/`, {
method: "DELETE",
})
if (!response.ok) {
throw new Error("Failed to delete notification")
}
return response.json()
}
export const markAllNotificationsRead = async (type?: string) => {
const response = await authFetch(`/api/notifications/seen/all/${buildSearchParams({ type })}`, {
method: "POST",
body: JSON.stringify(type ? { type } : {}),
})
if (!response.ok) {
throw new Error("Failed to mark all notifications as read")
}
return response.json()
}
export const issueNotificationStreamToken = async (): Promise<NotificationStreamTokenResponse> => {
const response = await authFetch("/api/notifications/stream-token/", {
method: "POST",
})
if (!response.ok) {
throw new Error("Failed to issue notification stream token")
}
return response.json()
}
export const buildNotificationStreamUrl = (token: string) => {
const cleanBaseUrl = API_BASE_URL.replace(/\/+$/, "")
return `${cleanBaseUrl}/api/notifications/stream/?token=${encodeURIComponent(token)}`
}
const REPORT_EXPORT_DOWNLOAD_PATTERN = /\/api\/reports\/exports\/[^/]+\/download\/?$/
const toApiEndpoint = (actionUrl: string) => {
const cleanBaseUrl = API_BASE_URL.replace(/\/+$/, "")
if (actionUrl.startsWith("http://") || actionUrl.startsWith("https://")) {
if (!actionUrl.startsWith(cleanBaseUrl)) {
return null
}
return actionUrl.slice(cleanBaseUrl.length) || "/"
}
return actionUrl.startsWith("/") ? actionUrl : `/${actionUrl}`
}
const getFilenameFromDisposition = (contentDisposition: string | null) => {
if (!contentDisposition) return null
const utfMatch = contentDisposition.match(/filename\*=UTF-8''([^;]+)/i)
if (utfMatch?.[1]) {
return decodeURIComponent(utfMatch[1])
}
const plainMatch = contentDisposition.match(/filename="?([^"]+)"?/i)
return plainMatch?.[1] || null
}
export const isReportExportDownloadUrl = (actionUrl?: string | null) => {
if (!actionUrl) return false
const endpoint = toApiEndpoint(actionUrl)
return !!endpoint && REPORT_EXPORT_DOWNLOAD_PATTERN.test(endpoint)
}
export const downloadNotificationFile = async (
actionUrl: string,
fallbackFilename?: string | null,
) => {
const endpoint = toApiEndpoint(actionUrl)
if (!endpoint) {
throw new Error("Unsupported download url")
}
const response = await authFetch(endpoint, {
method: "GET",
})
if (!response.ok) {
throw new Error("Failed to download file")
}
const blob = await response.blob()
const objectUrl = window.URL.createObjectURL(blob)
const filename =
getFilenameFromDisposition(response.headers.get("content-disposition")) ||
fallbackFilename ||
"download"
const anchor = document.createElement("a")
anchor.href = objectUrl
anchor.download = filename
document.body.appendChild(anchor)
anchor.click()
anchor.remove()
window.URL.revokeObjectURL(objectUrl)
}