CityTrack / User /src /services /issueService.ts
0xarchit's picture
User app beta v1 complete
71638d4
import { API_BASE_URL } from "../config/supabase";
import { Issue, IssueListResponse, LocationData } from "../types";
import EventSource, { EventSourceListener } from "react-native-sse";
import { cacheService } from "./cacheService";
class IssueService {
private baseUrl: string;
constructor() {
this.baseUrl = API_BASE_URL;
}
async createIssue(
imageUri: string,
location: LocationData,
description?: string,
accessToken?: string,
): Promise<{ issue_id: string; stream_url: string }> {
const formData = new FormData();
const imageFile = {
uri: imageUri,
type: "image/jpeg",
name: "issue_photo.jpg",
} as any;
formData.append("images", imageFile);
formData.append("latitude", location.latitude.toString());
formData.append("longitude", location.longitude.toString());
formData.append("accuracy_meters", location.accuracy.toString());
formData.append("platform", "mobile_app");
formData.append("device_model", "expo_device");
if (description) {
formData.append("description", description);
}
if (accessToken) {
formData.append("authorization", `Bearer ${accessToken}`);
console.log("[IssueService] Authorization token appended to form data");
} else {
console.warn("[IssueService] No access token provided - issue will be created without user_id");
}
const headers: Record<string, string> = {
"Content-Type": "multipart/form-data",
};
const response = await fetch(`${this.baseUrl}/issues/stream`, {
method: "POST",
headers,
body: formData,
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || "Failed to create issue");
}
await cacheService.clearCache();
return response.json();
}
async getIssue(issueId: string): Promise<Issue> {
const response = await fetch(`${this.baseUrl}/issues/${issueId}`);
if (!response.ok) {
throw new Error("Failed to fetch issue");
}
return response.json();
}
async listIssues(
page: number = 1,
pageSize: number = 20,
state?: string,
userId?: string,
): Promise<IssueListResponse> {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
});
if (state) {
params.append("state", state);
}
if (userId) {
params.append("user_id", userId);
}
const response = await fetch(`${this.baseUrl}/issues?${params}`);
if (!response.ok) {
throw new Error("Failed to fetch issues");
}
return response.json();
}
async connectToFlowStream(
issueId: string,
onMessage: (data: any) => void,
onError: (error: Error) => void,
): Promise<() => void> {
const eventSource = new EventSource(`${this.baseUrl}/flow/flow/${issueId}`);
const listener: EventSourceListener = (event) => {
if (event.type === "message") {
try {
const data = JSON.parse(event.data || "{}");
onMessage(data);
} catch (e) {
console.error("Failed to parse SSE message:", e);
}
} else if (event.type === "error") {
onError(new Error("SSE connection error"));
}
};
eventSource.addEventListener("message", listener);
eventSource.addEventListener("error", listener);
return () => {
eventSource.removeAllEventListeners();
eventSource.close();
};
}
async confirmIssue(issueId: string, confirmed: boolean): Promise<Issue> {
const response = await fetch(`${this.baseUrl}/issues/${issueId}/confirm`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ confirmed }),
});
if (!response.ok) {
throw new Error("Failed to confirm issue");
}
return response.json();
}
}
export const issueService = new IssueService();