1394 lines
41 KiB
TypeScript
1394 lines
41 KiB
TypeScript
// src/services/apiClient.ts
|
|
import { Profile, ShoppingListItem, SearchQuery, Budget, Address } from '../types';
|
|
import { logger } from './logger.client';
|
|
import { eventBus } from './eventBus';
|
|
|
|
// This constant should point to your backend API.
|
|
// It's often a good practice to store this in an environment variable.
|
|
// Using a relative path '/api' is the most robust method for production.
|
|
// It makes API calls to the same host that served the frontend files,
|
|
// which is then handled by the Nginx reverse proxy.
|
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api';
|
|
|
|
export interface ApiOptions {
|
|
tokenOverride?: string;
|
|
signal?: AbortSignal;
|
|
}
|
|
|
|
// --- API Fetch Wrapper with Token Refresh Logic ---
|
|
|
|
/**
|
|
* Helper to safely join a base URL and a path, ensuring exactly one slash between them.
|
|
* This preserves path segments in the base URL (e.g. '/api') which `new URL()` would strip
|
|
* if the path argument started with a slash.
|
|
*/
|
|
const joinUrl = (base: string, path: string): string => {
|
|
const cleanBase = base.replace(/\/+$/, '');
|
|
const cleanPath = path.replace(/^\/+/, '');
|
|
return `${cleanBase}/${cleanPath}`;
|
|
};
|
|
|
|
/**
|
|
* A promise that holds the in-progress token refresh operation.
|
|
* This prevents multiple parallel refresh requests.
|
|
*/
|
|
let refreshTokenPromise: Promise<string> | null = null;
|
|
|
|
/**
|
|
* Attempts to refresh the access token using the HttpOnly refresh token cookie.
|
|
* @returns A promise that resolves to the new access token.
|
|
*/
|
|
const refreshToken = async (): Promise<string> => {
|
|
logger.info('Attempting to refresh access token...');
|
|
try {
|
|
// Use the joinUrl helper for consistency, though usually this is a relative fetch in browser
|
|
const refreshUrl =
|
|
typeof window === 'undefined' && !API_BASE_URL.startsWith('http')
|
|
? joinUrl(API_BASE_URL, '/auth/refresh-token')
|
|
: `${API_BASE_URL}/auth/refresh-token`;
|
|
|
|
const response = await fetch(refreshUrl, {
|
|
method: 'POST',
|
|
// This endpoint relies on the HttpOnly cookie, so no body is needed.
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
const data = await response.json();
|
|
if (!response.ok) {
|
|
throw new Error(data.message || 'Failed to refresh token.');
|
|
}
|
|
// On successful refresh, store the new access token.
|
|
if (typeof window !== 'undefined') {
|
|
localStorage.setItem('authToken', data.token);
|
|
}
|
|
logger.info('Successfully refreshed access token.');
|
|
return data.token;
|
|
} catch (error) {
|
|
logger.error({ error }, 'Failed to refresh token. User session has expired.');
|
|
// Only perform browser-specific actions if in the browser environment.
|
|
if (typeof window !== 'undefined') {
|
|
localStorage.removeItem('authToken');
|
|
// Dispatch a global event that the UI layer can listen for to handle session expiry.
|
|
eventBus.dispatch('sessionExpired');
|
|
}
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* A custom fetch wrapper that handles automatic token refreshing.
|
|
* All authenticated API calls should use this function.
|
|
* @param url The URL to fetch.
|
|
* @param options The fetch options.
|
|
* @returns A promise that resolves to the fetch Response.
|
|
*/
|
|
export const apiFetch = async (
|
|
url: string,
|
|
options: RequestInit = {},
|
|
apiOptions: ApiOptions = {},
|
|
): Promise<Response> => {
|
|
// Always construct the full URL from the base and the provided path,
|
|
// unless the path is already a full URL. This works for both browser and Node.js.
|
|
const fullUrl = url.startsWith('http') ? url : joinUrl(API_BASE_URL, url);
|
|
|
|
logger.debug(`apiFetch: ${options.method || 'GET'} ${fullUrl}`);
|
|
|
|
// Create a new headers object to avoid mutating the original options.
|
|
const headers = new Headers(options.headers || {});
|
|
// Use the token override if provided (for testing), otherwise get it from localStorage.
|
|
// The `typeof window` check prevents errors in the Node.js test environment.
|
|
const token =
|
|
apiOptions.tokenOverride ??
|
|
(typeof window !== 'undefined' ? localStorage.getItem('authToken') : null);
|
|
if (token) {
|
|
headers.set('Authorization', `Bearer ${token}`);
|
|
}
|
|
|
|
// Do not set Content-Type for FormData. The browser must set it with the
|
|
// correct multipart boundary. For all other requests, default to application/json.
|
|
if (!(options.body instanceof FormData) && !headers.has('Content-Type')) {
|
|
headers.set('Content-Type', 'application/json');
|
|
}
|
|
|
|
const newOptions = { ...options, headers, signal: apiOptions.signal || options.signal };
|
|
|
|
let response = await fetch(fullUrl, newOptions);
|
|
|
|
if (response.status === 401) {
|
|
// Prevent an infinite loop if the refresh token endpoint itself returns 401.
|
|
if (fullUrl.includes('/auth/refresh-token')) {
|
|
return response;
|
|
}
|
|
|
|
try {
|
|
logger.info(`apiFetch: Received 401 for ${fullUrl}. Attempting token refresh.`);
|
|
// If no refresh is in progress, start one.
|
|
if (!refreshTokenPromise) {
|
|
refreshTokenPromise = refreshToken();
|
|
}
|
|
|
|
// Wait for the existing or new refresh operation to complete.
|
|
const newToken = await refreshTokenPromise;
|
|
|
|
logger.info(`apiFetch: Token refreshed. Retrying original request to ${fullUrl}.`);
|
|
// Retry the original request with the new token.
|
|
headers.set('Authorization', `Bearer ${newToken}`);
|
|
response = await fetch(fullUrl, { ...options, headers });
|
|
} catch (refreshError) {
|
|
// If refreshToken() fails, it will redirect, but we re-throw just in case.
|
|
return Promise.reject(refreshError);
|
|
} finally {
|
|
// Clear the promise so the next 401 will trigger a new refresh.
|
|
refreshTokenPromise = null;
|
|
}
|
|
}
|
|
|
|
// --- DEBUG LOGGING for failed requests ---
|
|
if (!response.ok) {
|
|
const responseText = await response.clone().text();
|
|
logger.error({ url: fullUrl, status: response.status, body: responseText },
|
|
'apiFetch: Request failed',
|
|
);
|
|
}
|
|
// --- END DEBUG LOGGING ---
|
|
|
|
return response;
|
|
};
|
|
|
|
/**
|
|
* Pings the backend server to check if it's running and reachable.
|
|
* @returns A promise that resolves to true if the server responds with 'pong'.
|
|
*/
|
|
export const pingBackend = async (): Promise<Response> => {
|
|
// This should return the response for the caller to handle.
|
|
return fetch(`${API_BASE_URL}/health/ping`);
|
|
};
|
|
|
|
/**
|
|
* Checks the backend's database schema.
|
|
* @returns A promise that resolves to an object with success status and a message.
|
|
*/
|
|
export const checkDbSchema = async (): Promise<Response> => {
|
|
return fetch(`${API_BASE_URL}/health/db-schema`);
|
|
};
|
|
|
|
/**
|
|
* Checks the backend's storage directory.
|
|
* @returns A promise that resolves to an object with success status and a message.
|
|
*/
|
|
export const checkStorage = async (): Promise<Response> => {
|
|
return fetch(`${API_BASE_URL}/health/storage`);
|
|
};
|
|
|
|
/**
|
|
* Checks the backend's database connection pool health.
|
|
* @returns A promise that resolves to an object with success status and a message.
|
|
*/
|
|
export const checkDbPoolHealth = async (): Promise<Response> => {
|
|
return fetch(`${API_BASE_URL}/health/db-pool`);
|
|
};
|
|
|
|
/**
|
|
* Checks the backend's Redis connection health.
|
|
* @returns A promise that resolves to an object with success status and a message.
|
|
*/
|
|
export const checkRedisHealth = async (): Promise<Response> => {
|
|
return fetch(`${API_BASE_URL}/health/redis`);
|
|
};
|
|
|
|
/**
|
|
* Checks the status of the application process managed by PM2.
|
|
* This is intended for development and diagnostic purposes.
|
|
*/
|
|
export const checkPm2Status = async (): Promise<Response> => {
|
|
// This is a public health check, so we can use standard fetch.
|
|
return fetch(`${API_BASE_URL}/system/pm2-status`);
|
|
};
|
|
|
|
/**
|
|
* Fetches all flyers from the backend.
|
|
* @returns A promise that resolves to an array of Flyer objects.
|
|
*/
|
|
export const fetchFlyers = async (): Promise<Response> => {
|
|
return fetch(`${API_BASE_URL}/flyers`);
|
|
};
|
|
|
|
/**
|
|
* Fetches a single flyer by its ID.
|
|
* @param flyerId The ID of the flyer to fetch.
|
|
* @returns A promise that resolves to the API response.
|
|
*/
|
|
export const fetchFlyerById = async (flyerId: number): Promise<Response> => {
|
|
return fetch(`${API_BASE_URL}/flyers/${flyerId}`);
|
|
};
|
|
|
|
/**
|
|
* Fetches all master grocery items from the backend.
|
|
* @returns A promise that resolves to an array of MasterGroceryItem objects.
|
|
*/
|
|
export const fetchMasterItems = async (): Promise<Response> => {
|
|
return fetch(`${API_BASE_URL}/personalization/master-items`);
|
|
};
|
|
|
|
/**
|
|
* Fetches all categories from the backend.
|
|
* @returns A promise that resolves to an array of Category objects.
|
|
*/
|
|
export const fetchCategories = async (): Promise<Response> => {
|
|
return fetch(`${API_BASE_URL}/categories`);
|
|
};
|
|
|
|
// --- Flyer Processing API Function ---
|
|
|
|
/**
|
|
* Uploads a flyer file to the backend to be processed asynchronously.
|
|
* @param file The flyer file (PDF or image).
|
|
* @param checksum The SHA-256 checksum of the file.
|
|
* @param tokenOverride Optional token for testing.
|
|
* @returns A promise that resolves to the API response, which should contain a `jobId`.
|
|
*/
|
|
export const uploadAndProcessFlyer = async (
|
|
file: File,
|
|
checksum: string,
|
|
tokenOverride?: string,
|
|
): Promise<Response> => {
|
|
const formData = new FormData();
|
|
formData.append('flyerFile', file);
|
|
formData.append('checksum', checksum);
|
|
return apiFetch(
|
|
'/ai/upload-and-process',
|
|
{
|
|
method: 'POST',
|
|
body: formData,
|
|
},
|
|
{ tokenOverride },
|
|
);
|
|
};
|
|
// --- Flyer Item API Functions ---
|
|
|
|
export const fetchFlyerItems = async (flyerId: number): Promise<Response> => {
|
|
return fetch(`${API_BASE_URL}/flyers/${flyerId}/items`);
|
|
};
|
|
|
|
export const fetchFlyerItemsForFlyers = async (flyerIds: number[]): Promise<Response> => {
|
|
if (flyerIds.length === 0) {
|
|
return new Response(JSON.stringify([]), { headers: { 'Content-Type': 'application/json' } });
|
|
}
|
|
return fetch(`${API_BASE_URL}/flyers/items/batch-fetch`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ flyerIds }),
|
|
});
|
|
};
|
|
|
|
export const countFlyerItemsForFlyers = async (flyerIds: number[]): Promise<Response> => {
|
|
if (flyerIds.length === 0) {
|
|
return new Response(JSON.stringify({ count: 0 }), {
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
}
|
|
return fetch(`${API_BASE_URL}/flyers/items/batch-count`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ flyerIds }),
|
|
});
|
|
};
|
|
|
|
// --- Store API Functions ---
|
|
|
|
/**
|
|
* Uploads a new logo for a store.
|
|
* @param storeId The ID of the store to update.
|
|
* @param logoImage The logo image file.
|
|
* @returns A promise that resolves with the new logo URL.
|
|
*/
|
|
export const uploadLogoAndUpdateStore = async (
|
|
storeId: number,
|
|
logoImage: File,
|
|
tokenOverride?: string,
|
|
): Promise<Response> => {
|
|
const formData = new FormData();
|
|
formData.append('logoImage', logoImage);
|
|
|
|
// Use apiFetch to ensure the user is authenticated to perform this action.
|
|
return apiFetch(
|
|
`/stores/${storeId}/logo`,
|
|
{
|
|
method: 'POST',
|
|
body: formData,
|
|
// Do not set Content-Type for FormData, browser handles it.
|
|
},
|
|
{ tokenOverride },
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Uploads a new logo for a brand. Requires admin privileges.
|
|
* @param brandId The ID of the brand to update.
|
|
* @param logoImage The logo image file.
|
|
* @returns A promise that resolves with the new logo URL.
|
|
*/
|
|
export const uploadBrandLogo = async (
|
|
brandId: number,
|
|
logoImage: File,
|
|
tokenOverride?: string,
|
|
): Promise<Response> => {
|
|
const formData = new FormData();
|
|
formData.append('logoImage', logoImage);
|
|
|
|
// Use apiFetch to ensure the user is an authenticated admin.
|
|
return apiFetch(
|
|
`/admin/brands/${brandId}/logo`,
|
|
{
|
|
method: 'POST',
|
|
body: formData,
|
|
// Do not set Content-Type for FormData, browser handles it.
|
|
},
|
|
{ tokenOverride },
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Fetches historical price data for a given list of master item IDs.
|
|
* @param masterItemIds An array of master grocery item IDs.
|
|
* @returns A promise that resolves to an array of historical price records.
|
|
*/
|
|
export const fetchHistoricalPriceData = async (
|
|
masterItemIds: number[],
|
|
tokenOverride?: string,
|
|
): Promise<Response> => {
|
|
if (masterItemIds.length === 0) {
|
|
// Return a Response with an empty array
|
|
return new Response(JSON.stringify([]), { headers: { 'Content-Type': 'application/json' } });
|
|
}
|
|
|
|
return apiFetch(
|
|
`/price-history`,
|
|
{
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ masterItemIds }),
|
|
},
|
|
{ tokenOverride },
|
|
);
|
|
};
|
|
|
|
// --- Watched Items API Functions ---
|
|
|
|
export const fetchWatchedItems = async (tokenOverride?: string): Promise<Response> => {
|
|
return apiFetch(`/users/watched-items`, {}, { tokenOverride });
|
|
};
|
|
|
|
export const addWatchedItem = async (
|
|
itemName: string,
|
|
category: string,
|
|
tokenOverride?: string,
|
|
): Promise<Response> => {
|
|
return apiFetch(
|
|
`/users/watched-items`,
|
|
{
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ itemName, category }),
|
|
},
|
|
{ tokenOverride },
|
|
);
|
|
};
|
|
|
|
export const removeWatchedItem = async (
|
|
masterItemId: number,
|
|
tokenOverride?: string,
|
|
): Promise<Response> => {
|
|
return apiFetch(
|
|
`/users/watched-items/${masterItemId}`,
|
|
{
|
|
method: 'DELETE',
|
|
},
|
|
{ tokenOverride },
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Fetches the best current sale prices for all of the user's watched items.
|
|
* @param tokenOverride Optional token for testing.
|
|
* @returns A promise that resolves to an array of WatchedItemDeal objects.
|
|
*/
|
|
export const fetchBestSalePrices = async (tokenOverride?: string): Promise<Response> => {
|
|
// This endpoint assumes an authenticated user session.
|
|
return apiFetch(`/users/deals/best-watched-prices`, {}, { tokenOverride });
|
|
};
|
|
|
|
// --- Shopping List API Functions ---
|
|
|
|
export const fetchShoppingLists = async (tokenOverride?: string): Promise<Response> => {
|
|
return apiFetch(`/users/shopping-lists`, {}, { tokenOverride });
|
|
};
|
|
|
|
export const fetchShoppingListById = async (
|
|
listId: number,
|
|
tokenOverride?: string,
|
|
): Promise<Response> => {
|
|
return apiFetch(`/users/shopping-lists/${listId}`, {}, { tokenOverride });
|
|
};
|
|
|
|
export const createShoppingList = async (
|
|
name: string,
|
|
tokenOverride?: string,
|
|
): Promise<Response> => {
|
|
return apiFetch(
|
|
`/users/shopping-lists`,
|
|
{
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name }),
|
|
},
|
|
{ tokenOverride },
|
|
);
|
|
};
|
|
|
|
export const deleteShoppingList = async (
|
|
listId: number,
|
|
tokenOverride?: string,
|
|
): Promise<Response> => {
|
|
return apiFetch(
|
|
`/users/shopping-lists/${listId}`,
|
|
{
|
|
method: 'DELETE',
|
|
},
|
|
{ tokenOverride },
|
|
);
|
|
};
|
|
|
|
export const addShoppingListItem = async (
|
|
listId: number,
|
|
item: { masterItemId?: number; customItemName?: string },
|
|
tokenOverride?: string,
|
|
): Promise<Response> => {
|
|
return apiFetch(
|
|
`/users/shopping-lists/${listId}/items`,
|
|
{
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(item),
|
|
},
|
|
{ tokenOverride },
|
|
);
|
|
};
|
|
|
|
export const updateShoppingListItem = async (
|
|
itemId: number,
|
|
updates: Partial<ShoppingListItem>,
|
|
tokenOverride?: string,
|
|
): Promise<Response> => {
|
|
return apiFetch(
|
|
`/users/shopping-lists/items/${itemId}`,
|
|
{
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(updates),
|
|
},
|
|
{ tokenOverride },
|
|
);
|
|
};
|
|
|
|
export const removeShoppingListItem = async (
|
|
itemId: number,
|
|
tokenOverride?: string,
|
|
): Promise<Response> => {
|
|
return apiFetch(
|
|
`/users/shopping-lists/items/${itemId}`,
|
|
{
|
|
method: 'DELETE',
|
|
},
|
|
{ tokenOverride },
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Fetches the full profile for the currently authenticated user.
|
|
* It retrieves the auth token from local storage and sends it in the Authorization header.
|
|
* @returns A promise that resolves to the user's combined UserProfile object.
|
|
* @throws An error if the request fails or if the user is not authenticated.
|
|
*/
|
|
export const getAuthenticatedUserProfile = async (options: ApiOptions = {}): Promise<Response> => {
|
|
// The token is now passed to apiFetch, which handles the Authorization header.
|
|
// If no token is provided (in browser context), apiFetch will get it from localStorage.
|
|
return apiFetch(
|
|
`/users/profile`,
|
|
{
|
|
method: 'GET',
|
|
},
|
|
options,
|
|
);
|
|
};
|
|
|
|
export async function loginUser(
|
|
email: string,
|
|
password: string,
|
|
rememberMe: boolean,
|
|
): Promise<Response> {
|
|
// This function already returns a Response, so it's correct.
|
|
// I'm just simplifying it slightly.
|
|
return fetch(`${API_BASE_URL}/auth/login`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ email, password, rememberMe }),
|
|
});
|
|
}
|
|
|
|
// --- Receipt Processing API Functions ---
|
|
|
|
/**
|
|
* Uploads a receipt image to the backend for processing.
|
|
* @param receiptImage The image file of the receipt.
|
|
* @returns A promise that resolves with the backend's response, including the newly created receipt record.
|
|
*/
|
|
export const uploadReceipt = async (
|
|
receiptImage: File,
|
|
tokenOverride?: string,
|
|
): Promise<Response> => {
|
|
const formData = new FormData();
|
|
formData.append('receiptImage', receiptImage);
|
|
|
|
// Use apiFetch, which now correctly handles FormData.
|
|
return apiFetch(
|
|
`/receipts/upload`,
|
|
{
|
|
method: 'POST',
|
|
body: formData,
|
|
},
|
|
{ tokenOverride },
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Fetches the deals found for a specific processed receipt.
|
|
* @param receiptId The ID of the processed receipt.
|
|
* @returns A promise that resolves to an array of ReceiptDeal objects.
|
|
*/
|
|
export const getDealsForReceipt = async (
|
|
receiptId: number,
|
|
tokenOverride?: string,
|
|
): Promise<Response> => {
|
|
return apiFetch(`/receipts/${receiptId}/deals`, {}, { tokenOverride });
|
|
};
|
|
|
|
// --- Analytics & Shopping Enhancement API Functions ---
|
|
|
|
export const trackFlyerItemInteraction = async (
|
|
itemId: number,
|
|
type: 'view' | 'click',
|
|
): Promise<void> => {
|
|
// Add 'return' here so the promise chain is returned to the caller
|
|
return apiFetch(`/flyer-items/${itemId}/track`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ type }),
|
|
keepalive: true, // Helps ensure the request is sent even if the page is closing
|
|
})
|
|
.then(() => {}) // Ensure return type is Promise<void>
|
|
.catch((error) => logger.warn('Failed to track flyer item interaction', { error }));
|
|
};
|
|
|
|
export const logSearchQuery = async (
|
|
query: Omit<SearchQuery, 'search_query_id' | 'id' | 'created_at' | 'user_id'>,
|
|
tokenOverride?: string,
|
|
): Promise<void> => {
|
|
// Add 'return' here
|
|
return apiFetch(
|
|
`/search/log`,
|
|
{
|
|
method: 'POST',
|
|
body: JSON.stringify(query),
|
|
keepalive: true,
|
|
},
|
|
{ tokenOverride },
|
|
)
|
|
.then(() => {}) // Ensure return type is Promise<void>
|
|
.catch((error) => logger.warn('Failed to log search query', { error }));
|
|
};
|
|
|
|
export const getPantryLocations = async (tokenOverride?: string): Promise<Response> => {
|
|
return apiFetch(`/pantry/locations`, {}, { tokenOverride });
|
|
};
|
|
|
|
export const createPantryLocation = async (
|
|
name: string,
|
|
tokenOverride?: string,
|
|
): Promise<Response> => {
|
|
return apiFetch(
|
|
`/pantry/locations`,
|
|
{
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name }),
|
|
},
|
|
{ tokenOverride },
|
|
);
|
|
};
|
|
|
|
export const completeShoppingList = async (
|
|
shoppingListId: number,
|
|
totalSpentCents?: number,
|
|
tokenOverride?: string,
|
|
): Promise<Response> => {
|
|
return apiFetch(
|
|
`/users/shopping-lists/${shoppingListId}/complete`,
|
|
{
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ totalSpentCents }),
|
|
},
|
|
{ tokenOverride },
|
|
);
|
|
};
|
|
|
|
export const getShoppingTripHistory = async (tokenOverride?: string): Promise<Response> => {
|
|
return apiFetch(`/users/shopping-history`, {}, { tokenOverride });
|
|
};
|
|
|
|
// --- Personalization & Social API Functions ---
|
|
|
|
export const getDietaryRestrictions = async (): Promise<Response> => {
|
|
return fetch(`${API_BASE_URL}/personalization/dietary-restrictions`);
|
|
};
|
|
|
|
export const getAppliances = async (): Promise<Response> => {
|
|
return fetch(`${API_BASE_URL}/personalization/appliances`);
|
|
};
|
|
|
|
export const getUserDietaryRestrictions = async (tokenOverride?: string): Promise<Response> => {
|
|
return apiFetch(`/users/me/dietary-restrictions`, {}, { tokenOverride });
|
|
};
|
|
|
|
export const setUserDietaryRestrictions = async (
|
|
restrictionIds: number[],
|
|
tokenOverride?: string,
|
|
): Promise<Response> => {
|
|
return apiFetch(
|
|
`/users/me/dietary-restrictions`,
|
|
{
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ restrictionIds }),
|
|
},
|
|
{ tokenOverride },
|
|
);
|
|
};
|
|
|
|
export const getCompatibleRecipes = async (tokenOverride?: string): Promise<Response> => {
|
|
return apiFetch(`/users/me/compatible-recipes`, {}, { tokenOverride });
|
|
};
|
|
|
|
export const getUserFeed = async (
|
|
limit: number = 20,
|
|
offset: number = 0,
|
|
tokenOverride?: string,
|
|
): Promise<Response> => {
|
|
return apiFetch(`/users/feed?limit=${limit}&offset=${offset}`, {}, { tokenOverride });
|
|
};
|
|
|
|
export const forkRecipe = async (
|
|
originalRecipeId: number,
|
|
tokenOverride?: string,
|
|
): Promise<Response> => {
|
|
return apiFetch(
|
|
`/recipes/${originalRecipeId}/fork`,
|
|
{
|
|
method: 'POST',
|
|
},
|
|
{ tokenOverride },
|
|
);
|
|
};
|
|
|
|
export const followUser = async (
|
|
userIdToFollow: string,
|
|
tokenOverride?: string,
|
|
): Promise<Response> => {
|
|
return apiFetch(
|
|
`/users/${userIdToFollow}/follow`,
|
|
{
|
|
method: 'POST',
|
|
},
|
|
{ tokenOverride },
|
|
);
|
|
};
|
|
|
|
export const unfollowUser = async (
|
|
userIdToUnfollow: string,
|
|
tokenOverride?: string,
|
|
): Promise<Response> => {
|
|
return apiFetch(
|
|
`/users/${userIdToUnfollow}/follow`,
|
|
{
|
|
method: 'DELETE',
|
|
},
|
|
{ tokenOverride },
|
|
);
|
|
};
|
|
|
|
// --- Activity Log API Function ---
|
|
|
|
/**
|
|
* Fetches a paginated list of recent activities from the backend.
|
|
* @param limit The number of items to fetch.
|
|
* @param offset The starting offset for pagination.
|
|
* @returns A promise that resolves to an array of ActivityLogItem objects.
|
|
*/
|
|
export const fetchActivityLog = async (
|
|
limit: number = 20,
|
|
offset: number = 0,
|
|
tokenOverride?: string,
|
|
): Promise<Response> => {
|
|
return apiFetch(`/admin/activity-log?limit=${limit}&offset=${offset}`, {}, { tokenOverride });
|
|
};
|
|
|
|
// --- Favorite Recipes API Functions ---
|
|
|
|
/**
|
|
* Retrieves a list of the currently authenticated user's favorite recipes.
|
|
* @param {string} [tokenOverride] Optional token for testing purposes.
|
|
* @returns {Promise<Response>} A promise that resolves to the API response.
|
|
*/
|
|
export const getUserFavoriteRecipes = async (tokenOverride?: string): Promise<Response> => {
|
|
return apiFetch(`/users/me/favorite-recipes`, {}, { tokenOverride });
|
|
};
|
|
|
|
export const addFavoriteRecipe = async (
|
|
recipeId: number,
|
|
tokenOverride?: string,
|
|
): Promise<Response> => {
|
|
return apiFetch(
|
|
`/users/me/favorite-recipes`,
|
|
{
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ recipeId }),
|
|
},
|
|
{ tokenOverride },
|
|
);
|
|
};
|
|
|
|
export const removeFavoriteRecipe = async (
|
|
recipeId: number,
|
|
tokenOverride?: string,
|
|
): Promise<Response> => {
|
|
return apiFetch(
|
|
`/users/me/favorite-recipes/${recipeId}`,
|
|
{
|
|
method: 'DELETE',
|
|
},
|
|
{ tokenOverride },
|
|
);
|
|
};
|
|
|
|
// --- Recipe Comments API Functions ---
|
|
|
|
export const getRecipeComments = async (recipeId: number): Promise<Response> => {
|
|
// This is a public endpoint, so we can use standard fetch.
|
|
return fetch(`${API_BASE_URL}/recipes/${recipeId}/comments`); // This was a duplicate, fixed.
|
|
};
|
|
|
|
/**
|
|
* Fetches a single recipe by its ID.
|
|
* @param recipeId The ID of the recipe to fetch.
|
|
* @returns A promise that resolves to the API response.
|
|
*/
|
|
export const getRecipeById = async (recipeId: number): Promise<Response> => {
|
|
return fetch(`${API_BASE_URL}/recipes/${recipeId}`);
|
|
};
|
|
|
|
export const addRecipeComment = async (
|
|
recipeId: number,
|
|
content: string,
|
|
parentCommentId?: number,
|
|
tokenOverride?: string,
|
|
): Promise<Response> => {
|
|
return apiFetch(
|
|
`/recipes/${recipeId}/comments`,
|
|
{
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ content, parentCommentId }),
|
|
},
|
|
{ tokenOverride },
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Deletes a recipe.
|
|
* @param recipeId The ID of the recipe to delete.
|
|
* @param tokenOverride Optional token for testing purposes.
|
|
* @returns {Promise<Response>} A promise that resolves to the API response.
|
|
*/
|
|
export const deleteRecipe = async (recipeId: number, tokenOverride?: string): Promise<Response> => {
|
|
return apiFetch(`/recipes/${recipeId}`, { method: 'DELETE' }, { tokenOverride });
|
|
};
|
|
// --- Admin API Functions for New Features ---
|
|
|
|
export const getUnmatchedFlyerItems = async (tokenOverride?: string): Promise<Response> => {
|
|
return apiFetch(`${API_BASE_URL}/admin/unmatched-items`, {}, { tokenOverride });
|
|
};
|
|
|
|
export const updateRecipeStatus = async (
|
|
recipeId: number,
|
|
status: 'private' | 'pending_review' | 'public' | 'rejected',
|
|
tokenOverride?: string,
|
|
): Promise<Response> => {
|
|
return apiFetch(
|
|
`/admin/recipes/${recipeId}/status`,
|
|
{
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ status }),
|
|
},
|
|
{ tokenOverride },
|
|
);
|
|
};
|
|
|
|
export const updateRecipeCommentStatus = async (
|
|
commentId: number,
|
|
status: 'visible' | 'hidden' | 'reported',
|
|
tokenOverride?: string,
|
|
): Promise<Response> => {
|
|
return apiFetch(
|
|
`/admin/comments/${commentId}/status`,
|
|
{
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ status }),
|
|
},
|
|
{ tokenOverride },
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Fetches all brands from the backend. Requires admin privileges.
|
|
* @returns A promise that resolves to an array of Brand objects.
|
|
*/
|
|
export const fetchAllBrands = async (tokenOverride?: string): Promise<Response> => {
|
|
return apiFetch(`/admin/brands`, {}, { tokenOverride });
|
|
};
|
|
export interface AppStats {
|
|
// This interface should ideally be in types.ts
|
|
flyerCount: number;
|
|
userCount: number;
|
|
flyerItemCount: number;
|
|
storeCount: number;
|
|
pendingCorrectionCount: number;
|
|
recipeCount: number;
|
|
}
|
|
|
|
export interface DailyStat {
|
|
date: string;
|
|
new_users: number;
|
|
new_flyers: number;
|
|
}
|
|
|
|
/**
|
|
* Fetches daily user registration and flyer upload stats for the last 30 days.
|
|
* @returns A promise that resolves to an array of daily stat objects.
|
|
*/
|
|
export const getDailyStats = async (tokenOverride?: string): Promise<Response> => {
|
|
return apiFetch(`${API_BASE_URL}/admin/stats/daily`, {}, { tokenOverride });
|
|
};
|
|
/**
|
|
* Fetches application-wide statistics. Requires admin privileges.
|
|
* @returns A promise that resolves to an object containing app stats.
|
|
*/
|
|
export const getApplicationStats = async (tokenOverride?: string): Promise<Response> => {
|
|
return apiFetch(`${API_BASE_URL}/admin/stats`, {}, { tokenOverride });
|
|
};
|
|
|
|
// --- Admin Correction API Functions ---
|
|
|
|
export const getSuggestedCorrections = async (tokenOverride?: string): Promise<Response> => {
|
|
return apiFetch(`${API_BASE_URL}/admin/corrections`, {}, { tokenOverride });
|
|
};
|
|
|
|
export const approveCorrection = async (
|
|
correctionId: number,
|
|
tokenOverride?: string,
|
|
): Promise<Response> => {
|
|
return apiFetch(
|
|
`/admin/corrections/${correctionId}/approve`,
|
|
{
|
|
method: 'POST',
|
|
},
|
|
{ tokenOverride },
|
|
);
|
|
};
|
|
|
|
export const rejectCorrection = async (
|
|
correctionId: number,
|
|
tokenOverride?: string,
|
|
): Promise<Response> => {
|
|
return apiFetch(
|
|
`/admin/corrections/${correctionId}/reject`,
|
|
{
|
|
method: 'POST',
|
|
},
|
|
{ tokenOverride },
|
|
);
|
|
};
|
|
|
|
export const updateSuggestedCorrection = async (
|
|
correctionId: number,
|
|
newSuggestedValue: string,
|
|
tokenOverride?: string,
|
|
): Promise<Response> => {
|
|
return apiFetch(
|
|
`/admin/corrections/${correctionId}`,
|
|
{
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ suggested_value: newSuggestedValue }),
|
|
},
|
|
{ tokenOverride },
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Enqueues a job to clean up the files associated with a specific flyer.
|
|
* Requires admin privileges.
|
|
* @param flyerId The ID of the flyer to clean up.
|
|
* @param tokenOverride Optional token for testing.
|
|
*/
|
|
export const cleanupFlyerFiles = async (
|
|
flyerId: number,
|
|
tokenOverride?: string,
|
|
): Promise<Response> => {
|
|
return apiFetch(`/admin/flyers/${flyerId}/cleanup`, { method: 'POST' }, { tokenOverride });
|
|
};
|
|
|
|
/**
|
|
* Enqueues a test job designed to fail, for testing the Bull Board UI.
|
|
* Requires admin privileges.
|
|
* @param tokenOverride Optional token for testing.
|
|
*/
|
|
export const triggerFailingJob = async (tokenOverride?: string): Promise<Response> => {
|
|
// This is an admin-only endpoint, so we use apiFetch to include the auth token.
|
|
return apiFetch(`/admin/trigger/failing-job`, { method: 'POST' }, { tokenOverride });
|
|
};
|
|
|
|
/**
|
|
* Fetches the status of a background processing job.
|
|
* @param jobId The ID of the job to check.
|
|
* @param tokenOverride Optional token for testing.
|
|
* @returns A promise that resolves to the API response with the job's status.
|
|
*/
|
|
export const getJobStatus = async (jobId: string, tokenOverride?: string): Promise<Response> => {
|
|
return apiFetch(`/ai/jobs/${jobId}/status`, {}, { tokenOverride });
|
|
};
|
|
|
|
/**
|
|
* Triggers the clearing of the geocoding cache on the server.
|
|
* Requires admin privileges.
|
|
* @param tokenOverride Optional token for testing.
|
|
*/
|
|
export const clearGeocodeCache = async (tokenOverride?: string): Promise<Response> => {
|
|
return apiFetch(`/admin/system/clear-geocode-cache`, { method: 'POST' }, { tokenOverride });
|
|
};
|
|
|
|
export async function registerUser(
|
|
email: string,
|
|
password: string,
|
|
fullName?: string,
|
|
avatarUrl?: string,
|
|
): Promise<Response> {
|
|
return fetch(`${API_BASE_URL}/auth/register`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ email, password, full_name: fullName, avatar_url: avatarUrl }),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Sends a password reset request for the given email to the backend.
|
|
* @param email The user's email address.
|
|
* @returns A promise that resolves with a success message.
|
|
*/
|
|
export async function requestPasswordReset(email: string): Promise<Response> {
|
|
return fetch(`${API_BASE_URL}/auth/forgot-password`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ email }),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Sends the password reset token and new password to the backend.
|
|
* @param token The password reset token from the URL.
|
|
* @param newPassword The user's new password.
|
|
* @returns A promise that resolves with a success message.
|
|
*/
|
|
export async function resetPassword(token: string, newPassword: string): Promise<Response> {
|
|
return fetch(`${API_BASE_URL}/auth/reset-password`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ token, newPassword }),
|
|
});
|
|
}
|
|
/**
|
|
* Sends updated user preferences to the backend.
|
|
* @param preferences A partial object of the user's preferences to update.
|
|
* @returns A promise that resolves to the user's full, updated profile object.
|
|
*/
|
|
export async function updateUserPreferences(
|
|
preferences: Partial<Profile['preferences']>,
|
|
apiOptions: ApiOptions = {},
|
|
): Promise<Response> {
|
|
return apiFetch(
|
|
`${API_BASE_URL}/users/profile/preferences`,
|
|
{
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(preferences),
|
|
},
|
|
apiOptions,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Sends updated user profile data (name, avatar) to the backend.
|
|
* @param profileData An object containing the full_name and/or avatar_url to update.
|
|
* @returns A promise that resolves to the user's full, updated profile object.
|
|
*/
|
|
export async function updateUserProfile(
|
|
profileData: Partial<Profile>,
|
|
apiOptions: ApiOptions = {},
|
|
): Promise<Response> {
|
|
return apiFetch(
|
|
`${API_BASE_URL}/users/profile`,
|
|
{
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(profileData),
|
|
},
|
|
apiOptions,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Fetches a complete export of the user's data from the backend.
|
|
* @returns A promise that resolves to a JSON object of the user's data.
|
|
*/
|
|
export async function exportUserData(apiOptions: ApiOptions = {}): Promise<Response> {
|
|
return apiFetch(
|
|
`${API_BASE_URL}/users/data-export`,
|
|
{
|
|
method: 'GET',
|
|
},
|
|
apiOptions,
|
|
);
|
|
}
|
|
|
|
export const getUserAppliances = async (tokenOverride?: string): Promise<Response> => {
|
|
return apiFetch(`/users/appliances`, {}, { tokenOverride });
|
|
};
|
|
|
|
/**
|
|
* Sets the kitchen appliances for the currently authenticated user.
|
|
* This will replace all existing appliances with the new set.
|
|
* @param applianceIds An array of numbers representing the IDs of the selected appliances.
|
|
*/
|
|
export const setUserAppliances = async (
|
|
applianceIds: number[],
|
|
apiOptions: ApiOptions = {},
|
|
): Promise<Response> => {
|
|
return apiFetch(
|
|
`/users/appliances`,
|
|
{
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ applianceIds }),
|
|
},
|
|
apiOptions,
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Sends an address string to the backend to be geocoded.
|
|
* @param address The full address string.
|
|
* @param tokenOverride Optional token for testing.
|
|
*/
|
|
export const geocodeAddress = async (
|
|
address: string,
|
|
apiOptions: ApiOptions = {},
|
|
): Promise<Response> => {
|
|
return apiFetch(
|
|
`/system/geocode`,
|
|
{
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ address }),
|
|
},
|
|
apiOptions,
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Fetches a specific address by its ID.
|
|
* @param addressId The ID of the address to fetch.
|
|
* @param tokenOverride Optional token for testing.
|
|
*/
|
|
export const getUserAddress = async (
|
|
addressId: number,
|
|
apiOptions: ApiOptions = {},
|
|
): Promise<Response> => {
|
|
return apiFetch(`/users/addresses/${addressId}`, {}, apiOptions);
|
|
};
|
|
|
|
/**
|
|
* Creates or updates the authenticated user's primary address.
|
|
* @param addressData The full address object.
|
|
* @param tokenOverride Optional token for testing.
|
|
*/
|
|
export const updateUserAddress = async (
|
|
addressData: Partial<Address>,
|
|
apiOptions: ApiOptions = {},
|
|
): Promise<Response> => {
|
|
return apiFetch(
|
|
`/users/profile/address`,
|
|
{
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(addressData),
|
|
},
|
|
apiOptions,
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Sends a new password to the backend to be updated.
|
|
* @param newPassword The user's new password.
|
|
* @returns A promise that resolves on success.
|
|
*/
|
|
export async function updateUserPassword(
|
|
newPassword: string,
|
|
apiOptions: ApiOptions = {},
|
|
): Promise<Response> {
|
|
return apiFetch(
|
|
`/users/profile/password`,
|
|
{
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ newPassword }),
|
|
},
|
|
apiOptions,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Updates the role of a specific user.
|
|
* @param userId The ID of the user to update.
|
|
* @param role The new role to assign ('user' or 'admin').
|
|
* @returns A promise that resolves to the updated Profile object.
|
|
*/
|
|
export async function updateUserRole(
|
|
userId: string,
|
|
role: 'user' | 'admin',
|
|
apiOptions: ApiOptions = {},
|
|
): Promise<Response> {
|
|
return apiFetch(
|
|
`/admin/users/${userId}/role`,
|
|
{
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ role }),
|
|
},
|
|
apiOptions,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Sends a request to delete the user's account, verifying with their current password.
|
|
* @param password The user's current password for verification.
|
|
* @returns A promise that resolves on success.
|
|
*/
|
|
export async function deleteUserAccount(
|
|
password: string,
|
|
apiOptions: ApiOptions = {},
|
|
): Promise<Response> {
|
|
return apiFetch(
|
|
`/users/account`,
|
|
{
|
|
method: 'DELETE',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ password }),
|
|
},
|
|
apiOptions,
|
|
);
|
|
}
|
|
|
|
// --- Notification API Functions ---
|
|
|
|
/**
|
|
* Fetches notifications for the authenticated user.
|
|
* @param limit The number of notifications to fetch.
|
|
* @param offset The number of notifications to skip for pagination.
|
|
* @returns A promise that resolves to the API response.
|
|
*/
|
|
export const getNotifications = async (
|
|
limit: number = 20,
|
|
offset: number = 0,
|
|
tokenOverride?: string,
|
|
): Promise<Response> => {
|
|
return apiFetch(`/users/notifications?limit=${limit}&offset=${offset}`, {}, { tokenOverride });
|
|
};
|
|
|
|
/**
|
|
* Marks all of the user's unread notifications as read.
|
|
* @returns A promise that resolves to the API response.
|
|
*/
|
|
export const markAllNotificationsAsRead = async (tokenOverride?: string): Promise<Response> => {
|
|
return apiFetch(`/users/notifications/mark-all-read`, { method: 'POST' }, { tokenOverride });
|
|
};
|
|
|
|
/**
|
|
* Marks a single notification as read.
|
|
* @param notificationId The ID of the notification to mark as read.
|
|
* @returns A promise that resolves to the API response.
|
|
*/
|
|
export const markNotificationAsRead = async (
|
|
notificationId: number,
|
|
tokenOverride?: string,
|
|
): Promise<Response> => {
|
|
return apiFetch(
|
|
`/users/notifications/${notificationId}/mark-read`,
|
|
{ method: 'POST' },
|
|
{ tokenOverride },
|
|
);
|
|
};
|
|
|
|
// --- Budgeting and Spending Analysis API Functions ---
|
|
|
|
/**
|
|
* Fetches all budgets for the authenticated user.
|
|
* @returns A promise that resolves to the API response.
|
|
*/
|
|
export const getBudgets = async (tokenOverride?: string): Promise<Response> => {
|
|
return apiFetch(`/budgets`, {}, { tokenOverride });
|
|
};
|
|
|
|
/**
|
|
* Creates a new budget for the authenticated user.
|
|
* @param budgetData The data for the new budget.
|
|
* @returns A promise that resolves to the API response.
|
|
*/
|
|
export const createBudget = async (
|
|
budgetData: Omit<Budget, 'budget_id' | 'user_id' | 'created_at' | 'updated_at'>,
|
|
tokenOverride?: string,
|
|
): Promise<Response> => {
|
|
return apiFetch(
|
|
`/budgets`,
|
|
{
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(budgetData),
|
|
},
|
|
{ tokenOverride },
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Updates an existing budget for the authenticated user.
|
|
* @param budgetId The ID of the budget to update.
|
|
* @param budgetData The data to update.
|
|
* @returns A promise that resolves to the API response.
|
|
*/
|
|
export const updateBudget = async (
|
|
budgetId: number,
|
|
budgetData: Partial<Omit<Budget, 'budget_id' | 'user_id' | 'created_at' | 'updated_at'>>,
|
|
tokenOverride?: string,
|
|
): Promise<Response> => {
|
|
return apiFetch(
|
|
`/budgets/${budgetId}`,
|
|
{
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(budgetData),
|
|
},
|
|
{ tokenOverride },
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Deletes a budget for the authenticated user.
|
|
* @param budgetId The ID of the budget to delete.
|
|
* @returns A promise that resolves to the API response.
|
|
*/
|
|
export const deleteBudget = async (budgetId: number, tokenOverride?: string): Promise<Response> => {
|
|
return apiFetch(`/budgets/${budgetId}`, { method: 'DELETE' }, { tokenOverride });
|
|
};
|
|
|
|
/**
|
|
* Fetches the user's spending analysis for a given date range.
|
|
* @param startDate The start date of the analysis period (YYYY-MM-DD).
|
|
* @param endDate The end date of the analysis period (YYYY-MM-DD).
|
|
* @returns A promise that resolves to the API response.
|
|
*/
|
|
export const getSpendingAnalysis = async (
|
|
startDate: string,
|
|
endDate: string,
|
|
tokenOverride?: string,
|
|
): Promise<Response> => {
|
|
return apiFetch(
|
|
`/budgets/spending-analysis?startDate=${startDate}&endDate=${endDate}`,
|
|
{},
|
|
{ tokenOverride },
|
|
);
|
|
};
|
|
|
|
// --- Gamification API Functions ---
|
|
|
|
/**
|
|
* Fetches the master list of all available achievements in the system.
|
|
* This is a public endpoint.
|
|
* @returns A promise that resolves to the API response.
|
|
*/
|
|
export const getAchievements = async (): Promise<Response> => {
|
|
// This is a public endpoint, so we can use standard fetch.
|
|
return fetch(`${API_BASE_URL}/achievements`);
|
|
};
|
|
|
|
/**
|
|
* Fetches all achievements earned by the currently authenticated user.
|
|
* @param tokenOverride Optional token for testing purposes.
|
|
* @returns A promise that resolves to the API response.
|
|
*/
|
|
export const getUserAchievements = async (tokenOverride?: string): Promise<Response> => {
|
|
return apiFetch(`/achievements/me`, {}, { tokenOverride });
|
|
};
|
|
|
|
/**
|
|
* Fetches the public leaderboard of top users by points.
|
|
* @param limit The number of users to fetch. Defaults to 10.
|
|
* @returns A promise that resolves to the API response.
|
|
*/
|
|
export const fetchLeaderboard = async (limit: number = 10): Promise<Response> => {
|
|
// This is a public endpoint, so we can use standard fetch.
|
|
return fetch(`${API_BASE_URL}/achievements/leaderboard?limit=${limit}`);
|
|
};
|
|
|
|
/**
|
|
* Uploads a new avatar image for the authenticated user.
|
|
* @param avatarFile The image file to upload.
|
|
* @param tokenOverride Optional token for testing purposes.
|
|
* @returns A promise that resolves to the API response containing the updated profile.
|
|
*/
|
|
export const uploadAvatar = async (avatarFile: File, tokenOverride?: string): Promise<Response> => {
|
|
const formData = new FormData();
|
|
formData.append('avatar', avatarFile);
|
|
|
|
// Use apiFetch, which now correctly handles FormData.
|
|
return apiFetch('/users/profile/avatar', { method: 'POST', body: formData }, { tokenOverride });
|
|
};
|