291 lines
7.6 KiB
TypeScript
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();
|
|
}
|