o2o-infinith-demo/src/lib/supabase.ts

291 lines
7.6 KiB
TypeScript

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<PipelineStatus> {
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<string, unknown>,
) {
// 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<string, unknown>[] }[] };
// 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();
}