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 } 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 => { 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 => { 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) }