diff --git a/lib/services/supabase_service.dart b/lib/services/supabase_service.dart index 8d4e8d2..1246198 100644 --- a/lib/services/supabase_service.dart +++ b/lib/services/supabase_service.dart @@ -1665,7 +1665,14 @@ class SupabaseService { final isAdmin = userProfile['role'] == 'admin'; // Create base query - final query = _client.from('tickets').select('*').eq('team_id', teamId); + // Using Supabase relational joins here completely eliminates the N+1 query problem. + // Instead of looping and querying `_getUserInfo` for each ticket individually, + // we fetch the creator and assignee info in a single optimized database request. + final query = _client.from('tickets').select(''' + *, + creator:users!tickets_created_by_fkey(id, full_name, role), + assignee:users!tickets_assigned_to_fkey(id, full_name, role) + ''').eq('team_id', teamId); // Filter by assignment if requested and user is not admin if (filterByAssignment && !isAdmin) { @@ -1685,30 +1692,15 @@ class SupabaseService { query.eq('priority', filterByPriority); } + // Execute the optimized query and order the results final response = await query.order('created_at', ascending: false); - // Process the response to add creator and assignee info + // Process the response into the format expected by the app final List> processedTickets = []; for (var ticket in response) { - final Map processedTicket = {...ticket}; - - // Add creator info - if (ticket['created_by'] != null) { - final creatorInfo = await _getUserInfo(ticket['created_by']); - if (creatorInfo != null) { - processedTicket['creator'] = creatorInfo; - } - } - - // Add assignee info - if (ticket['assigned_to'] != null) { - final assigneeInfo = await _getUserInfo(ticket['assigned_to']); - if (assigneeInfo != null) { - processedTicket['assignee'] = assigneeInfo; - } - } - - processedTickets.add(processedTicket); + // The relational join already handles embedding the creator and assignee data. + // We ensure we retain compatibility with the original returned data structure. + processedTickets.add({...ticket}); } return processedTickets; @@ -1818,12 +1810,50 @@ class SupabaseService { }; } + final createdTicket = Map.from(response[0]); + final ticketId = createdTicket['id']; + + try { + final functionsResponse = await _client.functions.invoke( + 'create-github-issue', + body: { + 'title': title, + 'description': description, + 'category': category, + 'priority': priority, + 'ticketId': ticketId, + }, + ); + + if (functionsResponse.status == 200) { + final responseData = functionsResponse.data; + if (responseData != null && responseData['success'] == true) { + final String? issueNumber = responseData['issueNumber']; + final String? issueUrl = responseData['issueUrl']; + + if (issueNumber != null && issueUrl != null) { + await _client.from('tickets').update({ + 'github_issue_number': issueNumber, + 'github_issue_url': issueUrl, + }).eq('id', ticketId); + + createdTicket['github_issue_number'] = issueNumber; + createdTicket['github_issue_url'] = issueUrl; + } + } + } else { + debugPrint('GitHub Edge Function returned status ${functionsResponse.status}'); + } + } catch (e) { + debugPrint('Failed to sync ticket to GitHub: $e'); + } + // Refresh tickets await getTickets(); return { 'success': true, - 'ticket': response[0], + 'ticket': createdTicket, }; } catch (e) { debugPrint('Error creating ticket: $e'); diff --git a/supabase/functions/create-github-issue/index.ts b/supabase/functions/create-github-issue/index.ts new file mode 100644 index 0000000..152d7b8 --- /dev/null +++ b/supabase/functions/create-github-issue/index.ts @@ -0,0 +1,72 @@ +import { serve } from "https://deno.land/std@0.168.0/http/server.ts" + +const corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', +} + +serve(async (req) => { + // Handle CORS preflight + if (req.method === 'OPTIONS') { + return new Response('ok', { headers: corsHeaders }) + } + + try { + const { title, description, category, priority, ticketId } = await req.json() + + const githubToken = Deno.env.get('GITHUB_ACCESS_TOKEN') + const repoOwner = Deno.env.get('GITHUB_REPO_OWNER') + const repoName = Deno.env.get('GITHUB_REPO_NAME') + + if (!githubToken || !repoOwner || !repoName) { + throw new Error("GitHub configuration is missing in environment variables.") + } + + const githubApiUrl = `https://api.github.com/repos/${repoOwner}/${repoName}/issues` + + const response = await fetch(githubApiUrl, { + method: "POST", + headers: { + "Accept": "application/vnd.github+json", + "Authorization": `Bearer ${githubToken}`, + "X-GitHub-Api-Version": "2022-11-28", + "Content-Type": "application/json" + }, + body: JSON.stringify({ + title: `[${category}] ${title}`, + body: `**Ticket ID:** ${ticketId}\n**Priority:** ${priority}\n\n${description || 'No description provided.'}`, + labels: [category?.toLowerCase() || 'bug', priority?.toLowerCase() || 'medium'] + }) + }) + + if (!response.ok) { + const errorText = await response.text() + console.error(`GitHub API Error: ${response.status} ${errorText}`) + throw new Error(`GitHub API failed with status ${response.status}`) + } + + const data = await response.json() + + return new Response( + JSON.stringify({ + success: true, + issueNumber: data.number.toString(), + issueUrl: data.html_url + }), + { + headers: { ...corsHeaders, "Content-Type": "application/json" }, + status: 200, + } + ) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + console.error("Edge Function Error:", errorMessage) + return new Response( + JSON.stringify({ success: false, error: errorMessage }), + { + headers: { ...corsHeaders, "Content-Type": "application/json" }, + status: 500, // Return 500 but handled gracefully by the client + } + ) + } +}) diff --git a/supabase/migrations/20260309152700_add_github_issue_to_tickets.sql b/supabase/migrations/20260309152700_add_github_issue_to_tickets.sql new file mode 100644 index 0000000..b0b1896 --- /dev/null +++ b/supabase/migrations/20260309152700_add_github_issue_to_tickets.sql @@ -0,0 +1,4 @@ +-- Migration to add GitHub issue tracking to tickets +ALTER TABLE tickets +ADD COLUMN github_issue_number TEXT, +ADD COLUMN github_issue_url TEXT;