diff --git a/api/applications.ts b/api/applications.ts new file mode 100644 index 00000000..d787a1fb --- /dev/null +++ b/api/applications.ts @@ -0,0 +1,345 @@ +import { createClient } from "@supabase/supabase-js"; + +const supabaseUrl = process.env.VITE_SUPABASE_URL || ""; +const supabaseServiceRole = process.env.SUPABASE_SERVICE_ROLE || ""; + +const supabase = createClient(supabaseUrl, supabaseServiceRole); + +export async function getMyApplications( + req: Request, + userId: string +) { + const url = new URL(req.url); + const page = parseInt(url.searchParams.get("page") || "1"); + const limit = parseInt(url.searchParams.get("limit") || "10"); + const status = url.searchParams.get("status"); + + try { + // Get creator ID for this user + const { data: creator } = await supabase + .from("aethex_creators") + .select("id") + .eq("user_id", userId) + .single(); + + if (!creator) { + return new Response( + JSON.stringify({ error: "Creator profile not found" }), + { status: 404, headers: { "Content-Type": "application/json" } } + ); + } + + let query = supabase + .from("aethex_applications") + .select( + ` + id, + creator_id, + opportunity_id, + status, + cover_letter, + response_message, + applied_at, + updated_at, + aethex_opportunities(id, title, arm_affiliation, job_type, posted_by_id, aethex_creators!aethex_opportunities_posted_by_id_fkey(username, avatar_url)) + `, + { count: "exact" } + ) + .eq("creator_id", creator.id); + + if (status) { + query = query.eq("status", status); + } + + query = query.order("applied_at", { ascending: false }); + + const start = (page - 1) * limit; + query = query.range(start, start + limit - 1); + + const { data, error, count } = await query; + + if (error) throw error; + + return new Response( + JSON.stringify({ + data, + pagination: { + page, + limit, + total: count || 0, + pages: Math.ceil((count || 0) / limit), + }, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + } catch (error) { + console.error("Error fetching applications:", error); + return new Response( + JSON.stringify({ error: "Failed to fetch applications" }), + { status: 500, headers: { "Content-Type": "application/json" } } + ); + } +} + +export async function getApplicationsForOpportunity( + opportunityId: string, + userId: string +) { + try { + // Verify user owns this opportunity + const { data: opportunity } = await supabase + .from("aethex_opportunities") + .select("posted_by_id") + .eq("id", opportunityId) + .single(); + + if (!opportunity) { + return new Response( + JSON.stringify({ error: "Opportunity not found" }), + { status: 404, headers: { "Content-Type": "application/json" } } + ); + } + + const { data: creator } = await supabase + .from("aethex_creators") + .select("id") + .eq("user_id", userId) + .single(); + + if (creator?.id !== opportunity.posted_by_id) { + return new Response( + JSON.stringify({ error: "Unauthorized" }), + { status: 403, headers: { "Content-Type": "application/json" } } + ); + } + + const { data, error } = await supabase + .from("aethex_applications") + .select( + ` + id, + creator_id, + status, + cover_letter, + applied_at, + aethex_creators(username, avatar_url, bio, skills) + ` + ) + .eq("opportunity_id", opportunityId) + .order("applied_at", { ascending: false }); + + if (error) throw error; + + return new Response(JSON.stringify({ data }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + console.error("Error fetching opportunity applications:", error); + return new Response( + JSON.stringify({ error: "Failed to fetch applications" }), + { status: 500, headers: { "Content-Type": "application/json" } } + ); + } +} + +export async function submitApplication( + req: Request, + userId: string +) { + try { + const body = await req.json(); + const { opportunity_id, cover_letter } = body; + + // Get creator ID for this user + const { data: creator } = await supabase + .from("aethex_creators") + .select("id") + .eq("user_id", userId) + .single(); + + if (!creator) { + return new Response( + JSON.stringify({ error: "Creator profile not found" }), + { status: 404, headers: { "Content-Type": "application/json" } } + ); + } + + // Check if opportunity exists and is open + const { data: opportunity } = await supabase + .from("aethex_opportunities") + .select("id") + .eq("id", opportunity_id) + .eq("status", "open") + .single(); + + if (!opportunity) { + return new Response( + JSON.stringify({ error: "Opportunity not found or closed" }), + { status: 404, headers: { "Content-Type": "application/json" } } + ); + } + + // Check if already applied + const { data: existing } = await supabase + .from("aethex_applications") + .select("id") + .eq("creator_id", creator.id) + .eq("opportunity_id", opportunity_id) + .single(); + + if (existing) { + return new Response( + JSON.stringify({ error: "You have already applied to this opportunity" }), + { status: 400, headers: { "Content-Type": "application/json" } } + ); + } + + const { data, error } = await supabase + .from("aethex_applications") + .insert({ + creator_id: creator.id, + opportunity_id, + cover_letter, + status: "submitted", + }) + .select() + .single(); + + if (error) throw error; + + return new Response(JSON.stringify(data), { + status: 201, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + console.error("Error submitting application:", error); + return new Response( + JSON.stringify({ error: "Failed to submit application" }), + { status: 500, headers: { "Content-Type": "application/json" } } + ); + } +} + +export async function updateApplicationStatus( + req: Request, + applicationId: string, + userId: string +) { + try { + const body = await req.json(); + const { status, response_message } = body; + + // Get the application and verify user owns the opportunity + const { data: application } = await supabase + .from("aethex_applications") + .select( + ` + id, + opportunity_id, + aethex_opportunities(posted_by_id) + ` + ) + .eq("id", applicationId) + .single(); + + if (!application) { + return new Response( + JSON.stringify({ error: "Application not found" }), + { status: 404, headers: { "Content-Type": "application/json" } } + ); + } + + const { data: creator } = await supabase + .from("aethex_creators") + .select("id") + .eq("user_id", userId) + .single(); + + if (creator?.id !== application.aethex_opportunities.posted_by_id) { + return new Response( + JSON.stringify({ error: "Unauthorized" }), + { status: 403, headers: { "Content-Type": "application/json" } } + ); + } + + const { data, error } = await supabase + .from("aethex_applications") + .update({ + status, + response_message, + updated_at: new Date().toISOString(), + }) + .eq("id", applicationId) + .select() + .single(); + + if (error) throw error; + + return new Response(JSON.stringify(data), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + console.error("Error updating application:", error); + return new Response( + JSON.stringify({ error: "Failed to update application" }), + { status: 500, headers: { "Content-Type": "application/json" } } + ); + } +} + +export async function withdrawApplication( + applicationId: string, + userId: string +) { + try { + // Verify user owns this application + const { data: application } = await supabase + .from("aethex_applications") + .select("creator_id") + .eq("id", applicationId) + .single(); + + if (!application) { + return new Response( + JSON.stringify({ error: "Application not found" }), + { status: 404, headers: { "Content-Type": "application/json" } } + ); + } + + const { data: creator } = await supabase + .from("aethex_creators") + .select("id") + .eq("user_id", userId) + .single(); + + if (creator?.id !== application.creator_id) { + return new Response( + JSON.stringify({ error: "Unauthorized" }), + { status: 403, headers: { "Content-Type": "application/json" } } + ); + } + + const { error } = await supabase + .from("aethex_applications") + .delete() + .eq("id", applicationId); + + if (error) throw error; + + return new Response(JSON.stringify({ success: true }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + console.error("Error withdrawing application:", error); + return new Response( + JSON.stringify({ error: "Failed to withdraw application" }), + { status: 500, headers: { "Content-Type": "application/json" } } + ); + } +}