import type { EntityName, ApiError, StockProduct, Product, SystemInfo, User } from '../types/grocy'; const STORAGE_KEY_SERVER = 'grocy_server_url'; const STORAGE_KEY_API = 'grocy_api_key'; class GrocyApiClient { private serverUrl: string | null = null; private apiKey: string | null = null; constructor() { // Load from localStorage on init this.serverUrl = localStorage.getItem(STORAGE_KEY_SERVER); this.apiKey = localStorage.getItem(STORAGE_KEY_API); } isConfigured(): boolean { return !!(this.serverUrl && this.apiKey); } getServerUrl(): string | null { return this.serverUrl; } configure(serverUrl: string, apiKey: string): void { // Normalize URL - remove trailing slash const normalizedUrl = serverUrl.replace(/\/+$/, ''); localStorage.setItem(STORAGE_KEY_SERVER, normalizedUrl); localStorage.setItem(STORAGE_KEY_API, apiKey); this.serverUrl = normalizedUrl; this.apiKey = apiKey; } clearConfig(): void { localStorage.removeItem(STORAGE_KEY_SERVER); localStorage.removeItem(STORAGE_KEY_API); this.serverUrl = null; this.apiKey = null; } private getBaseUrl(): string { // In development, use Vite proxy (relative URL) // In production or when configured, use absolute URL if (import.meta.env.DEV && !this.serverUrl) { return ''; } return this.serverUrl || ''; } private async request(method: string, endpoint: string, body?: unknown): Promise { const baseUrl = this.getBaseUrl(); const url = `${baseUrl}/api${endpoint}`; const headers: HeadersInit = { 'Content-Type': 'application/json', Accept: 'application/json', }; // Only add API key header if configured if (this.apiKey) { headers['GROCY-API-KEY'] = this.apiKey; } const response = await fetch(url, { method, headers, body: body ? JSON.stringify(body) : undefined, }); if (!response.ok) { const error: ApiError = await response.json().catch(() => ({ error_message: `HTTP ${response.status}: ${response.statusText}`, })); throw new Error(error.error_message); } if (response.status === 204) { return undefined as T; } return response.json(); } // Generic CRUD async getObjects(entity: EntityName, query?: string): Promise { const endpoint = `/objects/${entity}${query ? `?${query}` : ''}`; return this.request('GET', endpoint); } async getObject(entity: EntityName, id: number): Promise { return this.request('GET', `/objects/${entity}/${id}`); } async createObject(entity: EntityName, data: Partial): Promise<{ created_object_id: number }> { return this.request('POST', `/objects/${entity}`, data); } async updateObject(entity: EntityName, id: number, data: Partial): Promise { return this.request('PUT', `/objects/${entity}/${id}`, data); } async deleteObject(entity: EntityName, id: number): Promise { return this.request('DELETE', `/objects/${entity}/${id}`); } // Stock async getStock(): Promise { return this.request('GET', '/stock'); } async getStockProduct(productId: number): Promise { return this.request('GET', `/stock/products/${productId}`); } async getProductByBarcode(barcode: string): Promise<{ product: Product }> { return this.request('GET', `/stock/products/by-barcode/${barcode}`); } async addProduct( productId: number, amount: number, options?: { best_before_date?: string; price?: number; location_id?: number; } ) { return this.request('POST', `/stock/products/${productId}/add`, { amount, ...options, }); } async consumeProduct( productId: number, amount: number, options?: { spoiled?: boolean; stock_entry_id?: string; location_id?: number; } ) { return this.request('POST', `/stock/products/${productId}/consume`, { amount, ...options, }); } async openProduct(productId: number, amount: number, stockEntryId?: string) { return this.request('POST', `/stock/products/${productId}/open`, { amount, stock_entry_id: stockEntryId, }); } // Shopping list async addToShoppingList(productId: number, amount: number, listId = 1, note?: string) { return this.createObject('shopping_list', { product_id: productId, amount, shopping_list_id: listId, note, }); } async clearShoppingList(listId = 1) { return this.request('POST', `/stock/shoppinglist/clear`, { list_id: listId }); } async addMissingProductsToShoppingList(listId = 1) { return this.request('POST', `/stock/shoppinglist/add-missing-products`, { list_id: listId }); } // Chores async getChores(): Promise> { return this.request('GET', '/chores'); } async executeChore(choreId: number, trackedTime?: string): Promise { return this.request('POST', `/chores/${choreId}/execute`, { tracked_time: trackedTime || new Date().toISOString(), }); } // System async getSystemInfo(): Promise { return this.request('GET', '/system/info'); } async getDbChangedTime(): Promise<{ changed_time: string }> { return this.request('GET', '/system/db-changed-time'); } // User async getCurrentUser(): Promise { return this.request('GET', '/user'); } async getUserSettings(): Promise> { return this.request('GET', '/user/settings'); } } export const grocyApi = new GrocyApiClient(); export default grocyApi;