async function _fetch(method, path, body) { const opts = { method, headers: { 'Content-Type': 'application/json' }, credentials: 'include' }; if (body !== undefined) opts.body = JSON.stringify(body); const res = await fetch('/api' + path, opts); const data = await res.json(); if (!res.ok) { const err = new Error(data.message || data.error || `HTTP ${res.status}`); err.status = res.status; err.data = data; err.details = data.details || []; err.code = data.code; throw err; } return data; } const get = (path) => _fetch('GET', path); const post = (path, body) => _fetch('POST', path, body); const put = (path, body) => _fetch('PUT', path, body); const del = (path) => _fetch('DELETE', path); function filenameFromDisposition(value) { if (!value) return null; const match = value.match(/filename="?([^"]+)"?/i); return match ? match[1] : null; } export const api = { // Auth me: () => get('/auth/me'), authMode: () => get('/auth/mode'), login: (data) => post('/auth/login', data), logout: () => post('/auth/logout'), restoreMultiUserMode: () => post('/auth/restore-multi-user-mode'), changePassword: (data) => post('/auth/change-password', data), acknowledgePrivacy: () => post('/auth/acknowledge-privacy'), // Admin hasUsers: () => get('/admin/has-users'), adminUsers: () => get('/admin/users'), createUser: (data) => post('/admin/users', data), resetPassword: (id, data) => put(`/admin/users/${id}/password`, data), updateUserRole: (id, data) => put(`/admin/users/${id}/role`, data), deleteUser: (id) => del(`/admin/users/${id}`), authModeConfig: () => get('/admin/auth-mode'), setAuthMode: (data) => put('/admin/auth-mode', data), testOidcConfig: (data) => post('/admin/auth-mode/oidc-test', data), adminBackups: () => get('/admin/backups'), createAdminBackup: () => post('/admin/backups'), deleteAdminBackup: (id) => del(`/admin/backups/${encodeURIComponent(id)}`), restoreAdminBackup: (id) => post(`/admin/backups/${encodeURIComponent(id)}/restore`), adminBackupSettings: () => get('/admin/backups/settings'), saveAdminBackupSettings: (data) => put('/admin/backups/settings', data), runScheduledBackupNow: () => post('/admin/backups/run-scheduled-now'), adminCleanup: () => get('/admin/cleanup'), saveAdminCleanup: (data) => put('/admin/cleanup', data), runAdminCleanup: () => post('/admin/cleanup/run'), downloadAdminBackup: async (id) => { const res = await fetch(`/api/admin/backups/${encodeURIComponent(id)}/download`, { credentials: 'include', }); if (!res.ok) { let data = {}; try { data = await res.json(); } catch {} throw new Error(data.error || `HTTP ${res.status}`); } return { blob: await res.blob(), filename: filenameFromDisposition(res.headers.get('Content-Disposition')) || id, }; }, importAdminBackup: async (file) => { const res = await fetch('/api/admin/backups/import', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/octet-stream' }, body: file, }); const data = await res.json(); if (!res.ok) { const err = new Error(data.message || data.error || `HTTP ${res.status}`); err.status = res.status; err.data = data; err.details = data.details || []; err.code = data.code; throw err; } return data; }, // Notifications (admin) notifAdmin: () => get('/notifications/admin'), saveNotifAdmin: (data) => put('/notifications/admin', data), testEmail: (data) => post('/notifications/test', data), notifMe: () => get('/notifications/me'), saveNotifMe: (data) => put('/notifications/me', data), // Profile profile: () => get('/profile'), updateProfile: (data) => _fetch('PATCH', '/profile', data), profileSettings: () => get('/profile/settings'), updateProfileSettings: (data) => _fetch('PATCH', '/profile/settings', data), changeProfilePassword: (data) => post('/profile/change-password', data), profileExports: () => get('/profile/exports'), profileImportHistory: () => get('/profile/import-history'), // Tracker tracker: (y, m) => get(`/tracker?year=${y}&month=${m}`), upcomingBills: (days = 30) => get(`/tracker/upcoming?days=${days}`), // Bills bills: () => get('/bills'), allBills: () => get('/bills?inactive=true'), bill: (id) => get(`/bills/${id}`), createBill: (data) => post('/bills', data), updateBill: (id, data) => put(`/bills/${id}`, data), deleteBill: (id) => del(`/bills/${id}`), billPayments: (id, p, l) => get(`/bills/${id}/payments?page=${p||1}&limit=${l||20}`), billMonthlyState: (id, y, m) => get(`/bills/${id}/monthly-state?year=${y}&month=${m}`), saveBillMonthlyState: (id, data) => put(`/bills/${id}/monthly-state`, data), billHistoryRanges: (id) => get(`/bills/${id}/history-ranges`), createBillHistoryRange: (id, data) => post(`/bills/${id}/history-ranges`, data), updateBillHistoryRange: (id, rangeId, data) => put(`/bills/${id}/history-ranges/${rangeId}`, data), deleteBillHistoryRange: (id, rangeId) => del(`/bills/${id}/history-ranges/${rangeId}`), // Payments quickPay: (data) => post('/payments/quick', data), bulkPay: (items) => post('/payments/bulk', items), createPayment: (data) => post('/payments', data), updatePayment: (id, data) => put(`/payments/${id}`, data), deletePayment: (id) => del(`/payments/${id}`), restorePayment: (id) => post(`/payments/${id}/restore`), // Categories categories: () => get('/categories'), createCategory: (data) => post('/categories', data), updateCategory: (id, data) => put(`/categories/${id}`, data), deleteCategory: (id) => del(`/categories/${id}`), // Settings settings: () => get('/settings'), saveSettings: (data) => put('/settings', data), // Status status: () => get('/status'), // Version (public) version: () => get('/version'), releaseHistory: () => get('/version/history'), // Export (returns a URL to navigate to, not a fetch) exportUrl: (year, fmt) => `/api/export?year=${year}&format=${fmt||'csv'}`, // Spreadsheet Import previewSpreadsheetImport: async (file, options = {}) => { const params = new URLSearchParams(); if (options.parseAllSheets) params.set('parse_all_sheets', 'true'); if (options.defaultYear) params.set('year', String(options.defaultYear)); if (options.defaultMonth) params.set('month', String(options.defaultMonth)); const qs = params.toString(); const res = await fetch(`/api/import/spreadsheet/preview${qs ? `?${qs}` : ''}`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/octet-stream', ...(file.name ? { 'X-Filename': file.name } : {}), }, body: file, }); const data = await res.json(); if (!res.ok) { const err = new Error(data.message || data.error || `HTTP ${res.status}`); err.status = res.status; err.data = data; err.details = data.details || []; err.code = data.code; throw err; } return data; }, applySpreadsheetImport: (data) => post('/import/spreadsheet/apply', data), importHistory: () => get('/import/history'), // User SQLite import previewUserDbImport: async (file) => { const res = await fetch('/api/import/user-db/preview', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/octet-stream', ...(file.name ? { 'X-Filename': file.name } : {}), }, body: file, }); const data = await res.json(); if (!res.ok) { const err = new Error(data.message || data.error || `HTTP ${res.status}`); err.status = res.status; err.data = data; err.details = data.details || []; err.code = data.code; throw err; } return data; }, applyUserDbImport: (data) => post('/import/user-db/apply', data), };