Files
thrilltrack-explorer/src-old/services/reports/reportsService.ts

306 lines
8.1 KiB
TypeScript

/**
* 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<string> {
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<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
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<ServiceResponse<Report>> {
try {
// Handle both new format and legacy Supabase format
const payload = 'reported_entity_type' in data
? mapSubmitReportToBackend(data)
: data;
const report = await this.request<Report>('/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<ServiceResponse<PaginatedReports>> {
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<PaginatedReports>(
`/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<ServiceResponse<Report>> {
try {
const report = await this.request<Report>(`/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<ServiceResponse<Report>> {
try {
const payload: UpdateReportStatusData = {
status: status as any,
resolution_notes: resolutionNotes,
};
const report = await this.request<Report>(`/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<ServiceResponse<void>> {
try {
await this.request<void>(`/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<ServiceResponse<ReportStats>> {
try {
const stats = await this.request<ReportStats>('/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 };