mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 16:31:12 -05:00
306 lines
8.1 KiB
TypeScript
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 };
|