/** * Reports Service - API client for Django Reports System * Provides abstraction layer between frontend and Django backend */ import { supabase } from '@/integrations/supabase/client'; import { handleError } from '@/lib/errorHandler'; import { logger } from '@/lib/logger'; import type { Report, SubmitReportData, LegacySubmitReportData, ReportFilters, PaginatedReports, ReportStats, UpdateReportStatusData, ServiceResponse, } from './types'; import { mapSubmitReportToBackend } from './mappers'; /** * Reports Service Class * Handles all communication with Django Reports API endpoints */ class ReportsService { private baseUrl: string; constructor() { // Use environment variable or fallback to relative path this.baseUrl = import.meta.env.VITE_DJANGO_API_URL || '/api/v1'; logger.log('[ReportsService] Initialized with base URL:', this.baseUrl); } /** * Get authentication token from current Supabase session * @returns JWT token for Django API authentication * @throws Error if no active session */ private async getAuthToken(): Promise { const { data: { session }, error } = await supabase.auth.getSession(); if (error) { logger.error('[ReportsService] Failed to get session', { error }); throw new Error('Failed to retrieve authentication session'); } if (!session?.access_token) { logger.error('[ReportsService] No active session'); throw new Error('Authentication required. Please sign in.'); } return session.access_token; } /** * Make authenticated request to Django API * @param endpoint - API endpoint path (e.g., '/reports/') * @param options - Fetch options * @returns Parsed JSON response * @throws Error with details from Django API */ private async request( endpoint: string, options: RequestInit = {} ): Promise { const token = await this.getAuthToken(); const url = `${this.baseUrl}${endpoint}`; logger.log('[ReportsService] Making request', { method: options.method || 'GET', url, hasBody: !!options.body }); const response = await fetch(url, { ...options, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, ...options.headers, }, }); // Handle non-JSON responses (e.g., 204 No Content) if (response.status === 204) { return undefined as T; } const data = await response.json(); if (!response.ok) { // Django returns errors in { detail: "message" } format const errorMessage = data.detail || data.message || 'Request failed'; logger.error('[ReportsService] Request failed', { status: response.status, error: errorMessage, url, }); throw new Error(errorMessage); } logger.log('[ReportsService] Request successful', { status: response.status, url }); return data; } /** * Submit a new report * @param data - Report submission data (supports legacy Supabase format) * @returns Created report */ async submitReport( data: SubmitReportData | LegacySubmitReportData ): Promise> { try { // Handle both new format and legacy Supabase format const payload = 'reported_entity_type' in data ? mapSubmitReportToBackend(data) : data; const report = await this.request('/reports/', { method: 'POST', body: JSON.stringify(payload), }); return { success: true, data: report }; } catch (error) { const errorId = handleError(error, { action: 'Submit report', metadata: { entityType: 'entity_type' in data ? data.entity_type : data.reported_entity_type, reportType: data.report_type, }, }); return { success: false, error: error instanceof Error ? error.message : 'Failed to submit report', }; } } /** * List reports with optional filters and pagination * @param filters - Optional filters (status, type, entity) * @param page - Page number (1-indexed) * @param pageSize - Number of items per page * @returns Paginated list of reports */ async listReports( filters: ReportFilters = {}, page: number = 1, pageSize: number = 50 ): Promise> { try { const params = new URLSearchParams({ page: page.toString(), page_size: pageSize.toString(), }); // Add optional filters if (filters.status) params.append('status', filters.status); if (filters.report_type) params.append('report_type', filters.report_type); if (filters.entity_type) params.append('entity_type', filters.entity_type); if (filters.entity_id) params.append('entity_id', filters.entity_id); const data = await this.request( `/reports/?${params.toString()}` ); return { success: true, data }; } catch (error) { const errorId = handleError(error, { action: 'List reports', metadata: { filters, page, pageSize }, }); return { success: false, error: error instanceof Error ? error.message : 'Failed to list reports', }; } } /** * Get a single report by ID * @param id - Report UUID * @returns Report details */ async getReport(id: string): Promise> { try { const report = await this.request(`/reports/${id}/`); return { success: true, data: report }; } catch (error) { const errorId = handleError(error, { action: 'Get report', metadata: { reportId: id }, }); return { success: false, error: error instanceof Error ? error.message : 'Failed to get report', }; } } /** * Update report status (moderators only) * @param id - Report UUID * @param status - New status * @param resolutionNotes - Optional resolution notes * @returns Updated report */ async updateReportStatus( id: string, status: string, resolutionNotes?: string ): Promise> { try { const payload: UpdateReportStatusData = { status: status as any, resolution_notes: resolutionNotes, }; const report = await this.request(`/reports/${id}/`, { method: 'PATCH', body: JSON.stringify(payload), }); return { success: true, data: report }; } catch (error) { const errorId = handleError(error, { action: 'Update report status', metadata: { reportId: id, newStatus: status }, }); return { success: false, error: error instanceof Error ? error.message : 'Failed to update report', }; } } /** * Delete a report (moderators only) * @param id - Report UUID * @returns Success status */ async deleteReport(id: string): Promise> { try { await this.request(`/reports/${id}/`, { method: 'DELETE', }); return { success: true }; } catch (error) { const errorId = handleError(error, { action: 'Delete report', metadata: { reportId: id }, }); return { success: false, error: error instanceof Error ? error.message : 'Failed to delete report', }; } } /** * Get report statistics (moderators only) * @returns Report statistics */ async getStatistics(): Promise> { try { const stats = await this.request('/reports/stats/'); return { success: true, data: stats }; } catch (error) { const errorId = handleError(error, { action: 'Get report statistics', }); return { success: false, error: error instanceof Error ? error.message : 'Failed to get statistics', }; } } /** * Get base URL for debugging/testing * @returns Current base URL */ getBaseUrl(): string { return this.baseUrl; } } // Export singleton instance export const reportsService = new ReportsService(); // Export class for testing export { ReportsService };