Files
bambuddy/frontend/src/api/client.ts
T

850 lines
26 KiB
TypeScript

const API_BASE = '/api/v1';
async function request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const response = await fetch(`${API_BASE}${endpoint}`, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.detail || `HTTP ${response.status}`);
}
return response.json();
}
// Printer types
export interface Printer {
id: number;
name: string;
serial_number: string;
ip_address: string;
access_code: string;
model: string | null;
nozzle_count: number; // 1 or 2, auto-detected from MQTT
is_active: boolean;
auto_archive: boolean;
created_at: string;
updated_at: string;
}
export interface HMSError {
code: string;
module: number;
severity: number; // 1=fatal, 2=serious, 3=common, 4=info
}
export interface PrinterStatus {
id: number;
name: string;
connected: boolean;
state: string | null;
current_print: string | null;
subtask_name: string | null;
gcode_file: string | null;
progress: number | null;
remaining_time: number | null;
layer_num: number | null;
total_layers: number | null;
temperatures: {
bed?: number;
bed_target?: number;
nozzle?: number;
nozzle_target?: number;
chamber?: number;
} | null;
cover_url: string | null;
hms_errors: HMSError[];
}
export interface PrinterCreate {
name: string;
serial_number: string;
ip_address: string;
access_code: string;
model?: string;
auto_archive?: boolean;
}
// Archive types
export interface ArchiveDuplicate {
id: number;
print_name: string | null;
created_at: string;
match_type: 'exact' | 'similar'; // 'exact' = hash match, 'similar' = name match
}
export interface Archive {
id: number;
printer_id: number | null;
filename: string;
file_path: string;
file_size: number;
content_hash: string | null;
thumbnail_path: string | null;
timelapse_path: string | null;
duplicates: ArchiveDuplicate[] | null;
duplicate_count: number;
print_name: string | null;
print_time_seconds: number | null;
actual_time_seconds: number | null; // Computed from started_at/completed_at
time_accuracy: number | null; // Percentage: 100 = perfect, >100 = faster than estimated
filament_used_grams: number | null;
filament_type: string | null;
filament_color: string | null;
layer_height: number | null;
total_layers: number | null;
nozzle_diameter: number | null;
bed_temperature: number | null;
nozzle_temperature: number | null;
status: string;
started_at: string | null;
completed_at: string | null;
extra_data: Record<string, unknown> | null;
makerworld_url: string | null;
designer: string | null;
is_favorite: boolean;
tags: string | null;
notes: string | null;
cost: number | null;
photos: string[] | null;
failure_reason: string | null;
energy_kwh: number | null;
energy_cost: number | null;
created_at: string;
}
export interface ArchiveStats {
total_prints: number;
successful_prints: number;
failed_prints: number;
total_print_time_hours: number;
total_filament_grams: number;
total_cost: number;
prints_by_filament_type: Record<string, number>;
prints_by_printer: Record<string, number>;
average_time_accuracy: number | null;
time_accuracy_by_printer: Record<string, number> | null;
total_energy_kwh: number;
total_energy_cost: number;
}
export interface BulkUploadResult {
uploaded: number;
failed: number;
results: Array<{ filename: string; id: number; status: string }>;
errors: Array<{ filename: string; error: string }>;
}
// Settings types
export interface AppSettings {
auto_archive: boolean;
save_thumbnails: boolean;
capture_finish_photo: boolean;
default_filament_cost: number;
currency: string;
energy_cost_per_kwh: number;
energy_tracking_mode: 'print' | 'total';
}
export type AppSettingsUpdate = Partial<AppSettings>;
// Cloud types
export interface CloudAuthStatus {
is_authenticated: boolean;
email: string | null;
}
export interface CloudLoginResponse {
success: boolean;
needs_verification: boolean;
message: string;
}
export interface SlicerSetting {
setting_id: string;
name: string;
type: string;
version: string | null;
user_id: string | null;
updated_time: string | null;
}
export interface SlicerSettingsResponse {
filament: SlicerSetting[];
printer: SlicerSetting[];
process: SlicerSetting[];
}
export interface CloudDevice {
dev_id: string;
name: string;
dev_model_name: string | null;
dev_product_name: string | null;
online: boolean;
}
// Smart Plug types
export interface SmartPlug {
id: number;
name: string;
ip_address: string;
printer_id: number | null;
enabled: boolean;
auto_on: boolean;
auto_off: boolean;
off_delay_mode: 'time' | 'temperature';
off_delay_minutes: number;
off_temp_threshold: number;
username: string | null;
password: string | null;
last_state: string | null;
last_checked: string | null;
auto_off_executed: boolean; // True when auto-off was triggered after print
created_at: string;
updated_at: string;
}
export interface SmartPlugCreate {
name: string;
ip_address: string;
printer_id?: number | null;
enabled?: boolean;
auto_on?: boolean;
auto_off?: boolean;
off_delay_mode?: 'time' | 'temperature';
off_delay_minutes?: number;
off_temp_threshold?: number;
username?: string | null;
password?: string | null;
}
export interface SmartPlugUpdate {
name?: string;
ip_address?: string;
printer_id?: number | null;
enabled?: boolean;
auto_on?: boolean;
auto_off?: boolean;
off_delay_mode?: 'time' | 'temperature';
off_delay_minutes?: number;
off_temp_threshold?: number;
username?: string | null;
password?: string | null;
}
export interface SmartPlugEnergy {
power: number | null; // Current watts
voltage: number | null; // Volts
current: number | null; // Amps
today: number | null; // kWh used today
yesterday: number | null; // kWh used yesterday
total: number | null; // Total kWh
factor: number | null; // Power factor (0-1)
apparent_power: number | null; // VA
reactive_power: number | null; // VAr
}
export interface SmartPlugStatus {
state: string | null;
reachable: boolean;
device_name: string | null;
energy: SmartPlugEnergy | null;
}
export interface SmartPlugTestResult {
success: boolean;
state: string | null;
device_name: string | null;
}
// Print Queue types
export interface PrintQueueItem {
id: number;
printer_id: number;
archive_id: number;
position: number;
scheduled_time: string | null;
require_previous_success: boolean;
auto_off_after: boolean;
status: 'pending' | 'printing' | 'completed' | 'failed' | 'skipped' | 'cancelled';
started_at: string | null;
completed_at: string | null;
error_message: string | null;
created_at: string;
archive_name?: string | null;
archive_thumbnail?: string | null;
printer_name?: string | null;
}
export interface PrintQueueItemCreate {
printer_id: number;
archive_id: number;
scheduled_time?: string | null;
require_previous_success?: boolean;
auto_off_after?: boolean;
}
export interface PrintQueueItemUpdate {
printer_id?: number;
position?: number;
scheduled_time?: string | null;
require_previous_success?: boolean;
auto_off_after?: boolean;
}
// MQTT Logging types
export interface MQTTLogEntry {
timestamp: string;
topic: string;
direction: 'in' | 'out';
payload: Record<string, unknown>;
}
export interface MQTTLogsResponse {
logging_enabled: boolean;
logs: MQTTLogEntry[];
}
// K-Profile types
export interface KProfile {
slot_id: number;
extruder_id: number;
nozzle_id: string;
nozzle_diameter: string;
filament_id: string;
name: string;
k_value: string;
n_coef: string;
ams_id: number;
tray_id: number;
setting_id: string | null;
}
export interface KProfileCreate {
slot_id?: number; // Storage slot, 0 for new profiles
extruder_id?: number;
nozzle_id: string;
nozzle_diameter: string;
filament_id: string;
name: string;
k_value: string;
n_coef?: string;
ams_id?: number;
tray_id?: number;
setting_id?: string | null;
}
export interface KProfileDelete {
slot_id: number; // cali_idx - calibration index to delete
extruder_id: number;
nozzle_id: string; // e.g., "HH00-0.4"
nozzle_diameter: string; // e.g., "0.4"
filament_id: string; // Bambu filament identifier
}
export interface KProfilesResponse {
profiles: KProfile[];
nozzle_diameter: string;
}
// Notification Provider types
export type ProviderType = 'callmebot' | 'ntfy' | 'pushover' | 'telegram' | 'email';
export interface NotificationProvider {
id: number;
name: string;
provider_type: ProviderType;
enabled: boolean;
config: Record<string, unknown>;
// Print lifecycle events
on_print_start: boolean;
on_print_complete: boolean;
on_print_failed: boolean;
on_print_stopped: boolean;
on_print_progress: boolean;
// Printer status events
on_printer_offline: boolean;
on_printer_error: boolean;
on_filament_low: boolean;
// Quiet hours
quiet_hours_enabled: boolean;
quiet_hours_start: string | null;
quiet_hours_end: string | null;
// Printer filter
printer_id: number | null;
// Status tracking
last_success: string | null;
last_error: string | null;
last_error_at: string | null;
// Timestamps
created_at: string;
updated_at: string;
}
export interface NotificationProviderCreate {
name: string;
provider_type: ProviderType;
enabled?: boolean;
config: Record<string, unknown>;
// Print lifecycle events
on_print_start?: boolean;
on_print_complete?: boolean;
on_print_failed?: boolean;
on_print_stopped?: boolean;
on_print_progress?: boolean;
// Printer status events
on_printer_offline?: boolean;
on_printer_error?: boolean;
on_filament_low?: boolean;
// Quiet hours
quiet_hours_enabled?: boolean;
quiet_hours_start?: string | null;
quiet_hours_end?: string | null;
// Printer filter
printer_id?: number | null;
}
export interface NotificationProviderUpdate {
name?: string;
provider_type?: ProviderType;
enabled?: boolean;
config?: Record<string, unknown>;
// Print lifecycle events
on_print_start?: boolean;
on_print_complete?: boolean;
on_print_failed?: boolean;
on_print_stopped?: boolean;
on_print_progress?: boolean;
// Printer status events
on_printer_offline?: boolean;
on_printer_error?: boolean;
on_filament_low?: boolean;
// Quiet hours
quiet_hours_enabled?: boolean;
quiet_hours_start?: string | null;
quiet_hours_end?: string | null;
// Printer filter
printer_id?: number | null;
}
export interface NotificationTestRequest {
provider_type: ProviderType;
config: Record<string, unknown>;
}
export interface NotificationTestResponse {
success: boolean;
message: string;
}
// Provider-specific config types for reference
export interface CallMeBotConfig {
phone: string;
apikey: string;
}
export interface NtfyConfig {
server?: string;
topic: string;
auth_token?: string | null;
}
export interface PushoverConfig {
user_key: string;
app_token: string;
priority?: number;
}
export interface TelegramConfig {
bot_token: string;
chat_id: string;
}
export interface EmailConfig {
smtp_server: string;
smtp_port?: number;
username: string;
password: string;
from_email: string;
to_email: string;
use_tls?: boolean;
}
// API functions
export const api = {
// Printers
getPrinters: () => request<Printer[]>('/printers/'),
getPrinter: (id: number) => request<Printer>(`/printers/${id}`),
createPrinter: (data: PrinterCreate) =>
request<Printer>('/printers/', {
method: 'POST',
body: JSON.stringify(data),
}),
updatePrinter: (id: number, data: Partial<PrinterCreate>) =>
request<Printer>(`/printers/${id}`, {
method: 'PATCH',
body: JSON.stringify(data),
}),
deletePrinter: (id: number) =>
request<void>(`/printers/${id}`, { method: 'DELETE' }),
getPrinterStatus: (id: number) =>
request<PrinterStatus>(`/printers/${id}/status`),
connectPrinter: (id: number) =>
request<{ connected: boolean }>(`/printers/${id}/connect`, {
method: 'POST',
}),
disconnectPrinter: (id: number) =>
request<{ connected: boolean }>(`/printers/${id}/disconnect`, {
method: 'POST',
}),
// MQTT Debug Logging
enableMQTTLogging: (printerId: number) =>
request<{ logging_enabled: boolean }>(`/printers/${printerId}/logging/enable`, {
method: 'POST',
}),
disableMQTTLogging: (printerId: number) =>
request<{ logging_enabled: boolean }>(`/printers/${printerId}/logging/disable`, {
method: 'POST',
}),
getMQTTLogs: (printerId: number) =>
request<MQTTLogsResponse>(`/printers/${printerId}/logging`),
clearMQTTLogs: (printerId: number) =>
request<{ status: string }>(`/printers/${printerId}/logging`, {
method: 'DELETE',
}),
// Printer File Manager
getPrinterFiles: (printerId: number, path = '/') =>
request<{
path: string;
files: Array<{
name: string;
is_directory: boolean;
size: number;
path: string;
}>;
}>(`/printers/${printerId}/files?path=${encodeURIComponent(path)}`),
getPrinterFileDownloadUrl: (printerId: number, path: string) =>
`${API_BASE}/printers/${printerId}/files/download?path=${encodeURIComponent(path)}`,
deletePrinterFile: (printerId: number, path: string) =>
request<{ status: string; path: string }>(`/printers/${printerId}/files?path=${encodeURIComponent(path)}`, {
method: 'DELETE',
}),
getPrinterStorage: (printerId: number) =>
request<{ used_bytes: number | null; free_bytes: number | null }>(`/printers/${printerId}/storage`),
// Archives
getArchives: (printerId?: number, limit = 50, offset = 0) => {
const params = new URLSearchParams();
if (printerId) params.set('printer_id', String(printerId));
params.set('limit', String(limit));
params.set('offset', String(offset));
return request<Archive[]>(`/archives/?${params}`);
},
getArchive: (id: number) => request<Archive>(`/archives/${id}`),
updateArchive: (id: number, data: {
printer_id?: number | null;
print_name?: string;
is_favorite?: boolean;
tags?: string;
notes?: string;
cost?: number;
failure_reason?: string | null;
}) =>
request<Archive>(`/archives/${id}`, {
method: 'PATCH',
body: JSON.stringify(data),
}),
toggleFavorite: (id: number) =>
request<Archive>(`/archives/${id}/favorite`, { method: 'POST' }),
deleteArchive: (id: number) =>
request<void>(`/archives/${id}`, { method: 'DELETE' }),
getArchiveStats: () => request<ArchiveStats>('/archives/stats'),
getArchiveDuplicates: (id: number) =>
request<{ duplicates: ArchiveDuplicate[]; count: number }>(`/archives/${id}/duplicates`),
backfillContentHashes: () =>
request<{ updated: number; errors: Array<{ id: number; error: string }> }>('/archives/backfill-hashes', {
method: 'POST',
}),
getArchiveThumbnail: (id: number) => `${API_BASE}/archives/${id}/thumbnail`,
getArchiveDownload: (id: number) => `${API_BASE}/archives/${id}/download`,
getArchiveGcode: (id: number) => `${API_BASE}/archives/${id}/gcode`,
getArchiveTimelapse: (id: number) => `${API_BASE}/archives/${id}/timelapse`,
scanArchiveTimelapse: (id: number) =>
request<{
status: string;
message: string;
filename?: string;
available_files?: Array<{ name: string; path: string; size: number; mtime: string | null }>;
}>(`/archives/${id}/timelapse/scan`, {
method: 'POST',
}),
selectArchiveTimelapse: (id: number, filename: string) =>
request<{ status: string; message: string; filename: string }>(
`/archives/${id}/timelapse/select?filename=${encodeURIComponent(filename)}`,
{ method: 'POST' }
),
uploadArchiveTimelapse: async (archiveId: number, file: File): Promise<{ status: string; filename: string }> => {
const formData = new FormData();
formData.append('file', file);
const response = await fetch(`${API_BASE}/archives/${archiveId}/timelapse/upload`, {
method: 'POST',
body: formData,
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.detail || `HTTP ${response.status}`);
}
return response.json();
},
// Photos
getArchivePhotoUrl: (archiveId: number, filename: string) =>
`${API_BASE}/archives/${archiveId}/photos/${encodeURIComponent(filename)}`,
uploadArchivePhoto: async (archiveId: number, file: File): Promise<{ status: string; filename: string; photos: string[] }> => {
const formData = new FormData();
formData.append('file', file);
const response = await fetch(`${API_BASE}/archives/${archiveId}/photos`, {
method: 'POST',
body: formData,
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.detail || `HTTP ${response.status}`);
}
return response.json();
},
deleteArchivePhoto: (archiveId: number, filename: string) =>
request<{ status: string; photos: string[] | null }>(`/archives/${archiveId}/photos/${encodeURIComponent(filename)}`, {
method: 'DELETE',
}),
// QR Code
getArchiveQRCodeUrl: (archiveId: number, size = 200) =>
`${API_BASE}/archives/${archiveId}/qrcode?size=${size}`,
getArchiveCapabilities: (id: number) =>
request<{
has_model: boolean;
has_gcode: boolean;
build_volume: { x: number; y: number; z: number };
}>(`/archives/${id}/capabilities`),
// Project Page
getArchiveProjectPage: (id: number) =>
request<{
title: string | null;
description: string | null;
designer: string | null;
designer_user_id: string | null;
license: string | null;
copyright: string | null;
creation_date: string | null;
modification_date: string | null;
origin: string | null;
profile_title: string | null;
profile_description: string | null;
profile_cover: string | null;
profile_user_id: string | null;
profile_user_name: string | null;
design_model_id: string | null;
design_profile_id: string | null;
design_region: string | null;
model_pictures: Array<{ name: string; path: string; url: string }>;
profile_pictures: Array<{ name: string; path: string; url: string }>;
thumbnails: Array<{ name: string; path: string; url: string }>;
}>(`/archives/${id}/project-page`),
updateArchiveProjectPage: (id: number, data: {
title?: string;
description?: string;
designer?: string;
license?: string;
copyright?: string;
profile_title?: string;
profile_description?: string;
}) =>
request(`/archives/${id}/project-page`, {
method: 'PATCH',
body: JSON.stringify(data),
}),
getArchiveProjectImageUrl: (archiveId: number, imagePath: string) =>
`${API_BASE}/archives/${archiveId}/project-image/${encodeURIComponent(imagePath)}`,
getArchiveForSlicer: (id: number, filename: string) =>
`${API_BASE}/archives/${id}/file/${encodeURIComponent(filename.endsWith('.3mf') ? filename : filename + '.3mf')}`,
reprintArchive: (archiveId: number, printerId: number) =>
request<{ status: string; printer_id: number; archive_id: number; filename: string }>(
`/archives/${archiveId}/reprint?printer_id=${printerId}`,
{ method: 'POST' }
),
uploadArchive: async (file: File, printerId?: number): Promise<Archive> => {
const formData = new FormData();
formData.append('file', file);
const url = printerId
? `${API_BASE}/archives/upload?printer_id=${printerId}`
: `${API_BASE}/archives/upload`;
const response = await fetch(url, {
method: 'POST',
body: formData,
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.detail || `HTTP ${response.status}`);
}
return response.json();
},
uploadArchivesBulk: async (files: File[], printerId?: number): Promise<BulkUploadResult> => {
const formData = new FormData();
files.forEach((file) => formData.append('files', file));
const url = printerId
? `${API_BASE}/archives/upload-bulk?printer_id=${printerId}`
: `${API_BASE}/archives/upload-bulk`;
const response = await fetch(url, {
method: 'POST',
body: formData,
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.detail || `HTTP ${response.status}`);
}
return response.json();
},
// Settings
getSettings: () => request<AppSettings>('/settings/'),
updateSettings: (data: AppSettingsUpdate) =>
request<AppSettings>('/settings/', {
method: 'PUT',
body: JSON.stringify(data),
}),
resetSettings: () =>
request<AppSettings>('/settings/reset', { method: 'POST' }),
checkFfmpeg: () =>
request<{ installed: boolean; path: string | null }>('/settings/check-ffmpeg'),
// Cloud
getCloudStatus: () => request<CloudAuthStatus>('/cloud/status'),
cloudLogin: (email: string, password: string, region = 'global') =>
request<CloudLoginResponse>('/cloud/login', {
method: 'POST',
body: JSON.stringify({ email, password, region }),
}),
cloudVerify: (email: string, code: string) =>
request<CloudLoginResponse>('/cloud/verify', {
method: 'POST',
body: JSON.stringify({ email, code }),
}),
cloudSetToken: (access_token: string) =>
request<CloudAuthStatus>('/cloud/token', {
method: 'POST',
body: JSON.stringify({ access_token }),
}),
cloudLogout: () =>
request<{ success: boolean }>('/cloud/logout', { method: 'POST' }),
getCloudSettings: (version = '01.09.00.00') =>
request<SlicerSettingsResponse>(`/cloud/settings?version=${version}`),
getCloudSettingDetail: (settingId: string) =>
request<Record<string, unknown>>(`/cloud/settings/${settingId}`),
getCloudDevices: () => request<CloudDevice[]>('/cloud/devices'),
// Smart Plugs
getSmartPlugs: () => request<SmartPlug[]>('/smart-plugs/'),
getSmartPlug: (id: number) => request<SmartPlug>(`/smart-plugs/${id}`),
getSmartPlugByPrinter: (printerId: number) => request<SmartPlug | null>(`/smart-plugs/by-printer/${printerId}`),
createSmartPlug: (data: SmartPlugCreate) =>
request<SmartPlug>('/smart-plugs/', {
method: 'POST',
body: JSON.stringify(data),
}),
updateSmartPlug: (id: number, data: SmartPlugUpdate) =>
request<SmartPlug>(`/smart-plugs/${id}`, {
method: 'PATCH',
body: JSON.stringify(data),
}),
deleteSmartPlug: (id: number) =>
request<void>(`/smart-plugs/${id}`, { method: 'DELETE' }),
controlSmartPlug: (id: number, action: 'on' | 'off' | 'toggle') =>
request<{ success: boolean; action: string }>(`/smart-plugs/${id}/control`, {
method: 'POST',
body: JSON.stringify({ action }),
}),
getSmartPlugStatus: (id: number) =>
request<SmartPlugStatus>(`/smart-plugs/${id}/status`),
testSmartPlugConnection: (ip_address: string, username?: string | null, password?: string | null) =>
request<SmartPlugTestResult>('/smart-plugs/test-connection', {
method: 'POST',
body: JSON.stringify({ ip_address, username, password }),
}),
// Print Queue
getQueue: (printerId?: number, status?: string) => {
const params = new URLSearchParams();
if (printerId) params.set('printer_id', String(printerId));
if (status) params.set('status', status);
return request<PrintQueueItem[]>(`/queue/?${params}`);
},
getQueueItem: (id: number) => request<PrintQueueItem>(`/queue/${id}`),
addToQueue: (data: PrintQueueItemCreate) =>
request<PrintQueueItem>('/queue/', {
method: 'POST',
body: JSON.stringify(data),
}),
updateQueueItem: (id: number, data: PrintQueueItemUpdate) =>
request<PrintQueueItem>(`/queue/${id}`, {
method: 'PATCH',
body: JSON.stringify(data),
}),
removeFromQueue: (id: number) =>
request<{ message: string }>(`/queue/${id}`, { method: 'DELETE' }),
reorderQueue: (items: { id: number; position: number }[]) =>
request<{ message: string }>('/queue/reorder', {
method: 'POST',
body: JSON.stringify({ items }),
}),
cancelQueueItem: (id: number) =>
request<{ message: string }>(`/queue/${id}/cancel`, { method: 'POST' }),
stopQueueItem: (id: number) =>
request<{ message: string }>(`/queue/${id}/stop`, { method: 'POST' }),
// K-Profiles
getKProfiles: (printerId: number, nozzleDiameter = '0.4') =>
request<KProfilesResponse>(`/printers/${printerId}/kprofiles/?nozzle_diameter=${nozzleDiameter}`),
setKProfile: (printerId: number, profile: KProfileCreate) =>
request<{ success: boolean; message: string }>(`/printers/${printerId}/kprofiles/`, {
method: 'POST',
body: JSON.stringify(profile),
}),
deleteKProfile: (printerId: number, profile: KProfileDelete) =>
request<{ success: boolean; message: string }>(`/printers/${printerId}/kprofiles/`, {
method: 'DELETE',
body: JSON.stringify(profile),
}),
// Notification Providers
getNotificationProviders: () => request<NotificationProvider[]>('/notifications/'),
getNotificationProvider: (id: number) => request<NotificationProvider>(`/notifications/${id}`),
createNotificationProvider: (data: NotificationProviderCreate) =>
request<NotificationProvider>('/notifications/', {
method: 'POST',
body: JSON.stringify(data),
}),
updateNotificationProvider: (id: number, data: NotificationProviderUpdate) =>
request<NotificationProvider>(`/notifications/${id}`, {
method: 'PATCH',
body: JSON.stringify(data),
}),
deleteNotificationProvider: (id: number) =>
request<{ message: string }>(`/notifications/${id}`, { method: 'DELETE' }),
testNotificationProvider: (id: number) =>
request<NotificationTestResponse>(`/notifications/${id}/test`, { method: 'POST' }),
testNotificationConfig: (data: NotificationTestRequest) =>
request<NotificationTestResponse>('/notifications/test-config', {
method: 'POST',
body: JSON.stringify(data),
}),
};