import { createClient } from "@supabase/supabase-js"; const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY; export const supabase = createClient(supabaseUrl, supabaseAnonKey); /** Common headers for Edge Function calls (includes JWT auth) */ const fnHeaders = () => ({ "Content-Type": "application/json", "Authorization": `Bearer ${supabaseAnonKey}`, }); export async function generateMarketingReport(url: string, clinicName?: string) { const response = await fetch( `${supabaseUrl}/functions/v1/generate-report`, { method: "POST", headers: fnHeaders(), body: JSON.stringify({ url, clinicName }), } ); if (!response.ok) { throw new Error(`Report generation failed: ${response.statusText}`); } return response.json(); } export async function fetchReportById(reportId: string) { const { data, error } = await supabase .from("marketing_reports") .select("*") .eq("id", reportId) .single(); if (error) throw new Error(`Failed to fetch report: ${error.message}`); return data; } /** * Fetch pipeline status for a report. * Used by AnalysisLoadingPage to resume interrupted pipelines. */ export interface PipelineStatus { reportId: string; clinicId?: string; runId?: string; status: string; clinicName?: string; hasChannelData: boolean; hasReport: boolean; } export async function fetchPipelineStatus(reportId: string): Promise { const { data, error } = await supabase .from("marketing_reports") .select("id, status, clinic_name, channel_data, report") .eq("id", reportId) .single(); if (error || !data) throw new Error(`Report not found: ${error?.message}`); return { reportId: data.id, status: data.status || "unknown", clinicName: data.clinic_name, hasChannelData: !!data.channel_data && Object.keys(data.channel_data).length > 0, hasReport: !!data.report && Object.keys(data.report).length > 0, }; } export interface EnrichChannelsRequest { reportId: string; clinicName: string; instagramHandle?: string; instagramHandles?: string[]; youtubeChannelId?: string; facebookHandle?: string; address?: string; } /** * Fire-and-forget: triggers background channel enrichment. * Returns enrichment result when the Edge Function completes (~27s). */ export async function enrichChannels(params: EnrichChannelsRequest) { const response = await fetch( `${supabaseUrl}/functions/v1/enrich-channels`, { method: "POST", headers: fnHeaders(), body: JSON.stringify(params), } ); if (!response.ok) { throw new Error(`Channel enrichment failed: ${response.statusText}`); } return response.json(); } export async function scrapeWebsite(url: string, clinicName?: string) { const response = await fetch( `${supabaseUrl}/functions/v1/scrape-website`, { method: "POST", headers: fnHeaders(), body: JSON.stringify({ url, clinicName }), } ); if (!response.ok) { throw new Error(`Scraping failed: ${response.statusText}`); } return response.json(); } // ─── Pipeline V2 API Functions ─── /** * Phase 1: Discover & verify social channels from website URL. * Returns verified handles + reportId for subsequent phases. */ export async function discoverChannels(url: string, clinicName?: string) { const response = await fetch( `${supabaseUrl}/functions/v1/discover-channels`, { method: "POST", headers: fnHeaders(), body: JSON.stringify({ url, clinicName }), } ); const data = await response.json(); if (!response.ok) { const err = new Error(data.error || `Channel discovery failed: ${response.statusText}`); (err as Error & { code?: string; domain?: string }).code = data.error; (err as Error & { code?: string; domain?: string }).domain = data.domain; throw err; } return data; } /** * Phase 2: Collect all channel data using verified handles. * Reads verified_channels from DB, runs parallel API calls. */ export async function collectChannelData(reportId: string, clinicId?: string, runId?: string) { const response = await fetch( `${supabaseUrl}/functions/v1/collect-channel-data`, { method: "POST", headers: fnHeaders(), body: JSON.stringify({ reportId, clinicId, runId }), } ); if (!response.ok) { throw new Error(`Data collection failed: ${response.statusText}`); } return response.json(); } /** * Phase 3: Generate AI report using real collected data from DB. */ export async function generateReportV2(reportId: string, clinicId?: string, runId?: string) { const response = await fetch( `${supabaseUrl}/functions/v1/generate-report`, { method: "POST", headers: fnHeaders(), body: JSON.stringify({ reportId, clinicId, runId }), } ); if (!response.ok) { throw new Error(`Report generation failed: ${response.statusText}`); } return response.json(); } // ─── Pipeline V2 Phase 4: Content Plan ─── /** * Phase 4: Generate AI content strategy and calendar. * Reads report data from DB, calls Perplexity for strategy generation, * stores result in content_plans table. */ export async function generateContentPlan(reportId: string, clinicId?: string, runId?: string) { const response = await fetch( `${supabaseUrl}/functions/v1/generate-content-plan`, { method: "POST", headers: fnHeaders(), body: JSON.stringify({ reportId, clinicId, runId }), } ); if (!response.ok) { throw new Error(`Content plan generation failed: ${response.statusText}`); } return response.json(); } /** * Fetch the active content plan for a clinic. * Returns the most recent is_active=true plan with all JSONB columns. */ export async function fetchActiveContentPlan(clinicId: string) { const { data, error } = await supabase .from("content_plans") .select("*") .eq("clinic_id", clinicId) .eq("is_active", true) .order("created_at", { ascending: false }) .limit(1) .single(); if (error) return null; return data; } /** * Update a single calendar entry within a content plan's JSONB. * Uses Postgres JSONB path update. */ export async function updateCalendarEntry( planId: string, entryId: string, updates: Record, ) { // Read current calendar const { data: plan, error: readErr } = await supabase .from("content_plans") .select("calendar") .eq("id", planId) .single(); if (readErr || !plan) throw new Error(`Plan not found: ${readErr?.message}`); const calendar = plan.calendar as { weeks: { entries: Record[] }[] }; // Find and update entry for (const week of calendar.weeks) { for (let i = 0; i < week.entries.length; i++) { if (week.entries[i].id === entryId) { week.entries[i] = { ...week.entries[i], ...updates, isManualEdit: true }; break; } } } const { error: writeErr } = await supabase .from("content_plans") .update({ calendar }) .eq("id", planId); if (writeErr) throw new Error(`Update failed: ${writeErr.message}`); } /** * Trigger strategy adjustment for a clinic. * Compares current vs previous channel_snapshots, generates AI recommendations. */ export async function triggerStrategyAdjustment(clinicId: string) { const response = await fetch( `${supabaseUrl}/functions/v1/adjust-strategy`, { method: "POST", headers: fnHeaders(), body: JSON.stringify({ clinicId }), } ); if (!response.ok) { throw new Error(`Strategy adjustment failed: ${response.statusText}`); } return response.json(); }