diff --git a/supabase/functions/sync-application-to-sheets/index.ts b/supabase/functions/sync-application-to-sheets/index.ts index 49bca8d..7ed2a09 100644 --- a/supabase/functions/sync-application-to-sheets/index.ts +++ b/supabase/functions/sync-application-to-sheets/index.ts @@ -44,7 +44,75 @@ type RequestData = | UpdateApplicationOperation | ResultOperation; +/* + * --------------------------------------------------------- + * Get authenticated user's display name + * --------------------------------------------------------- + * + * The frontend currently sends "Admin" / "Panelist" as + * reviewedBy. We do not trust that value. + * + * Instead, use the authenticated Supabase user's ID to + * find the corresponding profile and obtain fullName. + * + * If the profile name is unavailable, fall back to the + * name stored in the authenticated user's metadata. + */ + +async function getReviewerName( + supabase: ReturnType, + user: any +): Promise { + const { + data: profile, + error: profileError, + } = await supabase + .from("profiles") + .select("fullName") + .eq("userId", user.id) + .maybeSingle(); + + if ( + !profileError && + profile?.fullName && + String(profile.fullName).trim() !== "" + ) { + return String(profile.fullName).trim(); + } + + if (profileError) { + console.error( + "Could not fetch reviewer profile:", + profileError + ); + } + + /* + * Fallback to the authenticated user's + * Google/Supabase metadata. + */ + const metadataName = + user.user_metadata?.full_name || + user.user_metadata?.name || + user.user_metadata?.fullName; + + if ( + metadataName && + String(metadataName).trim() !== "" + ) { + return String(metadataName).trim(); + } + + return "Unknown Reviewer"; +} + Deno.serve(async (req: Request) => { + /* + * --------------------------------------------------------- + * CORS + * --------------------------------------------------------- + */ + if (req.method === "OPTIONS") { return new Response("ok", { headers: corsHeaders, @@ -61,97 +129,89 @@ Deno.serve(async (req: Request) => { status: 405, headers: { ...corsHeaders, - "Content-Type": - "application/json", + "Content-Type": "application/json", }, } ); } try { + /* + * --------------------------------------------------------- + * 1. Authenticate Supabase user + * --------------------------------------------------------- + */ + const authorization = - req.headers.get( - "Authorization" - ); + req.headers.get("Authorization"); if (!authorization) { return new Response( JSON.stringify({ success: false, - error: - "Authentication required", + error: "Authentication required", }), { status: 401, headers: { ...corsHeaders, - "Content-Type": - "application/json", + "Content-Type": "application/json", }, } ); } const supabaseUrl = - Deno.env.get( - "SUPABASE_URL" - ); + Deno.env.get("SUPABASE_URL"); const supabaseAnonKey = - Deno.env.get( - "SUPABASE_ANON_KEY" - ); + Deno.env.get("SUPABASE_ANON_KEY"); - if ( - !supabaseUrl || - !supabaseAnonKey - ) { + if (!supabaseUrl || !supabaseAnonKey) { throw new Error( "Supabase environment variables are missing" ); } - const supabase = - createClient( - supabaseUrl, - supabaseAnonKey, - { - global: { - headers: { - Authorization: - authorization, - }, + const supabase = createClient( + supabaseUrl, + supabaseAnonKey, + { + global: { + headers: { + Authorization: authorization, }, - } - ); + }, + } + ); const { data: { user }, error: userError, - } = - await supabase.auth.getUser(); + } = await supabase.auth.getUser(); - if ( - userError || - !user - ) { + if (userError || !user) { return new Response( JSON.stringify({ success: false, - error: - "Invalid or expired Supabase session", + error: "Invalid or expired Supabase session", }), { status: 401, headers: { ...corsHeaders, - "Content-Type": - "application/json", + "Content-Type": "application/json", }, } ); } + /* + * --------------------------------------------------------- + * 2. Parse request + * --------------------------------------------------------- + */ + const data = (await req.json()) as RequestData; @@ -159,20 +219,24 @@ Deno.serve(async (req: Request) => { return new Response( JSON.stringify({ success: false, - error: - "Missing operation", + error: "Missing operation", }), { status: 400, headers: { ...corsHeaders, - "Content-Type": - "application/json", + "Content-Type": "application/json", }, } ); } + /* + * --------------------------------------------------------- + * 3. Google Apps Script configuration + * --------------------------------------------------------- + */ + const googleScriptUrl = Deno.env.get( "GOOGLE_SHEETS_SCRIPT_URL" @@ -198,10 +262,7 @@ Deno.serve(async (req: Request) => { * --------------------------------------------------------- */ - if ( - data.operation === - "application" - ) { + if (data.operation === "application") { const application = data as ApplicationOperation; @@ -217,20 +278,11 @@ Deno.serve(async (req: Request) => { "q4", ] as const; - for ( - const field of - requiredFields - ) { + for (const field of requiredFields) { if ( - application[ - field - ] === undefined || - application[ - field - ] === null || - String( - application[field] - ).trim() === "" + application[field] === undefined || + application[field] === null || + String(application[field]).trim() === "" ) { return new Response( JSON.stringify({ @@ -260,8 +312,7 @@ Deno.serve(async (req: Request) => { "application/json", }, body: JSON.stringify({ - operation: - "application", + operation: "application", applicationId: application.applicationId, @@ -327,11 +378,9 @@ Deno.serve(async (req: Request) => { return new Response( JSON.stringify({ success: true, - operation: - "application", + operation: "application", duplicate: - googleResult.duplicate ?? - false, + googleResult.duplicate ?? false, }), { status: 200, @@ -349,7 +398,9 @@ Deno.serve(async (req: Request) => { * UPDATE APPLICATION * --------------------------------------------------------- * - * Updates only Name / Phone / SID / Branch. + * Updates only: + * Name / Phone / SID / Branch + * * Submitted answers are never changed. */ @@ -411,18 +462,11 @@ Deno.serve(async (req: Request) => { "branch", ] as const; - for ( - const field of - requiredFields - ) { + for (const field of requiredFields) { if ( - update[field] === - undefined || - update[field] === - null || - String( - update[field] - ).trim() === "" + update[field] === undefined || + update[field] === null || + String(update[field]).trim() === "" ) { return new Response( JSON.stringify({ @@ -524,12 +568,16 @@ Deno.serve(async (req: Request) => { * --------------------------------------------------------- * RESULT * --------------------------------------------------------- + * + * The reviewer name is determined server-side from the + * authenticated Supabase user. + * + * We intentionally do NOT use result.reviewedBy here. + * The frontend may send "Panelist", but the Edge Function + * replaces it with the authenticated user's actual name. */ - if ( - data.operation === - "result" - ) { + if (data.operation === "result") { const result = data as ResultOperation; @@ -556,6 +604,18 @@ Deno.serve(async (req: Request) => { ); } + /* + * ----------------------------------------------------- + * Get actual reviewer name + * ----------------------------------------------------- + */ + + const reviewerName = + await getReviewerName( + supabase, + user + ); + const response = (applicant as any) .applicant_response; @@ -566,8 +626,13 @@ Deno.serve(async (req: Request) => { : response; const branch = - responseData?.branch || - ""; + responseData?.branch || ""; + + /* + * ----------------------------------------------------- + * Send result to Google Apps Script + * ----------------------------------------------------- + */ const googleResponse = await fetch( @@ -579,8 +644,7 @@ Deno.serve(async (req: Request) => { "application/json", }, body: JSON.stringify({ - operation: - "result", + operation: "result", applicationId: result.applicationId, @@ -589,8 +653,7 @@ Deno.serve(async (req: Request) => { applicant.name, phone: - applicant.phone || - "", + applicant.phone || "", sid: applicant.sid, @@ -601,11 +664,10 @@ Deno.serve(async (req: Request) => { result.status, remarks: - result.remarks || - "", + result.remarks || "", reviewedBy: - result.reviewedBy, + reviewerName, reviewedAt: result.reviewedAt, @@ -646,8 +708,9 @@ Deno.serve(async (req: Request) => { return new Response( JSON.stringify({ success: true, - operation: - "result", + operation: "result", + reviewedBy: + reviewerName, }), { status: 200, @@ -660,11 +723,16 @@ Deno.serve(async (req: Request) => { ); } + /* + * --------------------------------------------------------- + * Unsupported operation + * --------------------------------------------------------- + */ + return new Response( JSON.stringify({ success: false, - error: - "Unsupported operation", + error: "Unsupported operation", }), { status: 400,