initial commit: rocket.new export of broadcastbeat
This commit is contained in:
616
src/app/api/admin/show-calendar/route.ts
Normal file
616
src/app/api/admin/show-calendar/route.ts
Normal file
@@ -0,0 +1,616 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { hybridAI } from '@/lib/ai/hybridRouter';
|
||||
|
||||
// ─── Authorized BB Pen Names (Section 15H / Conflict 4 resolved) ─────────────
|
||||
const BB_PEN_NAMES = [
|
||||
{ name: 'Michael Strand', beat: 'broadcast-technology', title: 'Senior Technology Editor' },
|
||||
{ name: 'David Harlow', beat: 'broadcast-engineering', title: 'Broadcast Engineering Correspondent' },
|
||||
{ name: 'Karen Fielding', beat: 'post-production', title: 'Post Production Editor' },
|
||||
{ name: 'James Mercer', beat: 'nab-show', title: 'NAB Show Senior Correspondent' },
|
||||
{ name: 'Peter Calloway', beat: 'radio-transmission', title: 'Radio & Transmission Reporter' },
|
||||
{ name: 'Sandra Voss', beat: 'production-technology', title: 'Production Technology Editor' },
|
||||
{ name: 'Brian Kowalski', beat: 'streaming-ott', title: 'Streaming & OTT Correspondent' },
|
||||
{ name: 'Laura Pennington', beat: 'industry-news', title: 'Industry News Editor' },
|
||||
{ name: 'Thomas Reeves', beat: 'audio-technology', title: 'Audio Technology Correspondent' },
|
||||
{ name: 'Christine Vale', beat: 'business-industry', title: 'Business & Industry Reporter' },
|
||||
{ name: 'Marcus Webb', beat: 'live-production', title: 'Live Production Correspondent' },
|
||||
{ name: 'Ellen Forsythe', beat: 'international-markets', title: 'International Markets Editor' },
|
||||
];
|
||||
|
||||
// ─── AV Beat Pen Names (authorized roster) ───────────────────────────────────
|
||||
const AV_PEN_NAMES = [
|
||||
{ name: 'Rex Chandler', beat: 'av-integration', title: 'AV Integration Senior Correspondent' },
|
||||
{ name: 'Dana Flux', beat: 'digital-signage', title: 'Digital Signage & Display Editor' },
|
||||
{ name: 'Derek Wainwright', beat: 'unified-communications', title: 'Unified Communications Correspondent' },
|
||||
{ name: 'Sloane Rigging', beat: 'sound-audio', title: 'Installed AV & Audio Editor' },
|
||||
{ name: 'Chip Crosspoint', beat: 'av-networking', title: 'AV Networking & Technology Correspondent' },
|
||||
{ name: 'Blair Presenter', beat: 'education-corporate-av', title: 'Education & Corporate AV Editor' },
|
||||
{ name: 'Jordan Lumen', beat: 'display-visualization', title: 'Display & Visualization Correspondent' },
|
||||
];
|
||||
|
||||
// Beat-to-story-type mapping for smart pen name selection
|
||||
const BEAT_STORY_MAP: Record<string, string[]> = {
|
||||
'nab-show': ['exhibitor_preview','show_preview_guide','show_floor_report','best_of_roundup','recap'],
|
||||
'broadcast-technology': ['product_announcement','breaking_announcement','company_news'],
|
||||
'broadcast-engineering': ['product_announcement','product_update'],
|
||||
'post-production': ['product_announcement','thought_leadership'],
|
||||
'audio-technology': ['product_announcement','event_coverage'],
|
||||
'live-production': ['show_floor_report','event_coverage'],
|
||||
'streaming-ott': ['product_announcement','company_news'],
|
||||
'industry-news': ['executive_news','company_news','press_release_rewrite'],
|
||||
'business-industry': ['thought_leadership','company_news'],
|
||||
'international-markets': ['show_floor_report','recap','exhibitor_preview'],
|
||||
'av-integration': ['product_announcement','company_news','exhibitor_preview'],
|
||||
'digital-signage': ['product_announcement','product_update'],
|
||||
'unified-communications':['product_announcement','company_news'],
|
||||
'display-visualization': ['product_announcement','product_update'],
|
||||
'education-corporate-av':['thought_leadership','company_news'],
|
||||
};
|
||||
|
||||
function pickPenNameForShow(
|
||||
siteId: number,
|
||||
storyType: string,
|
||||
recentNames: string[],
|
||||
preferredName?: string
|
||||
): string {
|
||||
const pool = siteId === 2 ? AV_PEN_NAMES : BB_PEN_NAMES;
|
||||
|
||||
// Use preferred name if specified and not overused
|
||||
if (preferredName) {
|
||||
const preferred = pool.find(p => p.name === preferredName);
|
||||
if (preferred && !recentNames.slice(-3).includes(preferredName)) {
|
||||
return preferredName;
|
||||
}
|
||||
}
|
||||
|
||||
// Find writers whose beat matches this story type
|
||||
const beatMatches = pool.filter(p => {
|
||||
const beatStories = BEAT_STORY_MAP[p.beat] || [];
|
||||
return beatStories.includes(storyType) && !recentNames.slice(-3).includes(p.name);
|
||||
});
|
||||
|
||||
if (beatMatches.length > 0) {
|
||||
return beatMatches[Math.floor(Math.random() * beatMatches.length)].name;
|
||||
}
|
||||
|
||||
// Fallback: any available writer
|
||||
const available = pool.filter(p => !recentNames.slice(-3).includes(p.name));
|
||||
return available.length > 0
|
||||
? available[Math.floor(Math.random() * available.length)].name
|
||||
: pool[0].name;
|
||||
}
|
||||
|
||||
// ─── Determine current show phase ────────────────────────────────────────────
|
||||
function getShowPhase(event: any): string {
|
||||
if (!event.start_date) return 'phase5_off_season';
|
||||
const now = new Date();
|
||||
const start = new Date(event.start_date);
|
||||
const end = new Date(event.end_date || event.start_date);
|
||||
const engineStart = new Date(event.story_engine_start_date || start);
|
||||
const engineEnd = new Date(event.story_engine_end_date || end);
|
||||
const daysToStart = Math.floor((start.getTime() - now.getTime()) / 86400000);
|
||||
const daysFromEnd = Math.floor((now.getTime() - end.getTime()) / 86400000);
|
||||
|
||||
if (now < engineStart) return 'phase5_off_season';
|
||||
if (daysToStart > 13) return 'phase1_buildup';
|
||||
if (daysToStart > 0) return 'phase2_surge';
|
||||
if (now >= start && now <= end) return 'phase3_show_week';
|
||||
if (daysFromEnd <= 14) return 'phase4_post_show';
|
||||
if (now > engineEnd) return 'phase5_off_season';
|
||||
return 'phase5_off_season';
|
||||
}
|
||||
|
||||
// ─── Generate show story via Hybrid AI ───────────────────────────────────────
|
||||
async function generateShowStory(params: {
|
||||
penName: string;
|
||||
penTitle: string;
|
||||
siteName: string;
|
||||
eventName: string;
|
||||
year: number;
|
||||
storyType: string;
|
||||
companyName?: string;
|
||||
sourceContent?: string;
|
||||
styleGuide?: string;
|
||||
phase: string;
|
||||
}): Promise<{ title: string; excerpt: string; content: string; metaDescription: string; confidence: number }> {
|
||||
const { penName, penTitle, siteName, eventName, year, storyType, companyName, sourceContent, styleGuide, phase } = params;
|
||||
|
||||
const styleContext = styleGuide
|
||||
? `\n\nSTYLE GUIDE (based on existing ${siteName} coverage):\n${styleGuide.slice(0, 800)}`
|
||||
: '';
|
||||
|
||||
const storyInstructions: Record<string, string> = {
|
||||
exhibitor_preview: `Write an exhibitor preview: "${companyName || 'Company'} to Showcase Solutions at ${eventName} ${year}". 400-600 words. SEO target: "${companyName} ${eventName.split(' ')[0]} ${year}".`,
|
||||
show_preview_guide: `Write a comprehensive show preview guide: "${eventName} ${year}: What to Expect". 800-1200 words. This is pillar content — high SEO value. Target keyword: "${eventName} ${year}".`,
|
||||
breaking_announcement: `Write a breaking news announcement: "${companyName || 'Company'} Launches at ${eventName} ${year}". 300-500 words. Publish within 2 hours of announcement. Lead with the news hook immediately.`,
|
||||
show_floor_report: `Write a show floor report for ${eventName} ${year}. 800-1200 words. Aggregate the day's key announcements. Include product highlights, company news, and show atmosphere.`,
|
||||
best_of_roundup: `Write a best-of roundup: "Best of ${eventName} ${year}: The Products That Stood Out". 1000-1500 words. Comprehensive post-show analysis.`,
|
||||
recap: `Write a show recap: "${eventName} ${year} Recap: Key Takeaways". 800-1200 words. Synthesize the show's major themes and announcements.`,
|
||||
award_announcement: `Write an award announcement story. 300-500 words. Professional tone. Include award significance and recipient background.`,
|
||||
product_announcement: `Write a product announcement: "${companyName || 'Company'} Announces New Product for ${eventName} ${year}". 400-700 words.`,
|
||||
keynote_preview: `Write a keynote preview story. 300-500 words. Include speaker background and session significance.`,
|
||||
travel_logistics: `Write a travel and logistics guide for ${eventName} ${year}. 600-900 words. Registration, hotels, transportation, key dates.`,
|
||||
product_availability: `Write a post-show product availability story. 300-500 words. Cover pricing, availability dates, and ordering information.`,
|
||||
press_release_rewrite: `Rewrite the following press release as a real news story for ${siteName}. Remove marketing language, lead with the news hook, add industry context. Byline: ${penName}.`,
|
||||
company_news: `Write a company news story. 300-500 words. Professional journalistic tone.`,
|
||||
executive_news: `Write an executive appointment/news story. 300-500 words.`,
|
||||
thought_leadership: `Write a thought leadership piece. 600-900 words. Frame as editorial analysis, not a press release.`,
|
||||
};
|
||||
|
||||
const instruction = storyInstructions[storyType] || storyInstructions.company_news;
|
||||
|
||||
const systemPrompt = `You are ${penName}, ${penTitle}, writing for ${siteName}. You are covering ${eventName} ${year}.
|
||||
|
||||
Rules:
|
||||
- Write in your own words — do not copy source text verbatim
|
||||
- Write as a real journalist, not a press release
|
||||
- Lead with the news hook, not company boilerplate
|
||||
- Include the year ${year} in the title
|
||||
- Professional journalistic tone appropriate for ${siteName}'s B2B trade audience
|
||||
- Do not mention or link to any competitor publications
|
||||
- End with a brief company boilerplate paragraph if company-specific
|
||||
- Return ONLY valid JSON with no markdown code blocks${styleContext}`;
|
||||
|
||||
const userPrompt = `${instruction}
|
||||
|
||||
${sourceContent ? `Source material:\n${sourceContent.slice(0, 2000)}` : `Write based on your knowledge of ${eventName} and the ${siteName} industry.`}
|
||||
|
||||
Return JSON: {"title":"...","excerpt":"...","content":"<p>...</p><p>...</p>","metaDescription":"150-160 char SEO description","confidence":0.0-1.0}`;
|
||||
|
||||
try {
|
||||
let result = await hybridAI(
|
||||
[
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: userPrompt },
|
||||
],
|
||||
{ maxTokens: 2000, isBackground: true, priority: 3 }
|
||||
);
|
||||
|
||||
const match = result.text.match(/\{[\s\S]*\}/);
|
||||
if (match) {
|
||||
const parsed = JSON.parse(match[0]);
|
||||
return {
|
||||
title: parsed.title || `${eventName} ${year}: Industry Update`,
|
||||
excerpt: parsed.excerpt || '',
|
||||
content: parsed.content || `<p>Coverage of ${eventName} ${year}.</p>`,
|
||||
metaDescription: parsed.metaDescription || `${eventName} ${year} coverage from ${siteName}.`,
|
||||
confidence: Math.min(1, Math.max(0, parsed.confidence || 0.75)),
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Show story generation error:', err);
|
||||
}
|
||||
|
||||
return {
|
||||
title: `${eventName} ${year}: Industry Update`,
|
||||
excerpt: `Coverage of ${eventName} ${year}.`,
|
||||
content: `<p>This is an AI-drafted story about ${eventName} ${year}.</p>`,
|
||||
metaDescription: `${eventName} ${year} coverage from ${siteName}.`,
|
||||
confidence: 0.5,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Extract Ryan Salazar style ───────────────────────────────────────────────
|
||||
async function extractStyleProfile(articles: any[], eventName: string): Promise<string> {
|
||||
if (articles.length === 0) return '';
|
||||
|
||||
const sampleContent = articles
|
||||
.slice(0, 5)
|
||||
.map(a => `TITLE: ${a.title}\n\nCONTENT: ${(a.content || '').slice(0, 500)}`)
|
||||
.join('\n\n---\n\n');
|
||||
|
||||
try {
|
||||
let result = await hybridAI(
|
||||
[
|
||||
{
|
||||
role: 'system',
|
||||
content: 'You are a writing style analyst. Analyze the provided articles and extract a concise style guide.',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `Analyze these articles covering ${eventName}. Extract: (1) typical article structure, (2) tone and voice, (3) common section types, (4) typical word count range, (5) how companies/products are introduced, (6) how show context is woven in. Return a concise style guide in 300-400 words.\n\n${sampleContent}`,
|
||||
},
|
||||
],
|
||||
{ maxTokens: 800, isBackground: true, priority: 6 }
|
||||
);
|
||||
return result.text;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// ─── GET ──────────────────────────────────────────────────────────────────────
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const supabase = await createClient();
|
||||
const { searchParams } = new URL(request.url);
|
||||
const action = searchParams.get('action');
|
||||
const tab = searchParams.get('tab') || 'calendar';
|
||||
const siteFilter = searchParams.get('site') || '';
|
||||
const showFilter = searchParams.get('show') || '';
|
||||
const statusFilter = searchParams.get('status') || '';
|
||||
|
||||
// ── Event Calendar ──
|
||||
if (tab === 'calendar' || action === 'calendar') {
|
||||
let query = supabase
|
||||
.from('event_calendar')
|
||||
.select('*')
|
||||
.order('start_date', { ascending: true, nullsFirst: false });
|
||||
if (siteFilter) query = query.eq('site_id', parseInt(siteFilter));
|
||||
const { data, error } = await query;
|
||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
|
||||
// Enrich with current phase
|
||||
const enriched = (data || []).map(event => ({
|
||||
...event,
|
||||
current_phase: getShowPhase(event),
|
||||
}));
|
||||
return NextResponse.json({ events: enriched });
|
||||
}
|
||||
|
||||
// ── Story Queue ──
|
||||
if (tab === 'queue') {
|
||||
let query = supabase
|
||||
.from('auto_story_queue')
|
||||
.select('*')
|
||||
.not('event_id', 'is', null)
|
||||
.order('created_at', { ascending: false });
|
||||
if (siteFilter) query = query.eq('site_id', parseInt(siteFilter));
|
||||
if (showFilter) query = query.eq('event_id', showFilter);
|
||||
if (statusFilter && statusFilter !== 'all') query = query.eq('status', statusFilter);
|
||||
const { data, error } = await query;
|
||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
return NextResponse.json({ queue: data || [] });
|
||||
}
|
||||
|
||||
// ── Exhibitor List ──
|
||||
if (tab === 'exhibitors') {
|
||||
let query = supabase
|
||||
.from('nab_exhibitors')
|
||||
.select('*, tracked_companies(company_website)')
|
||||
.order('created_at', { ascending: false });
|
||||
if (showFilter) query = query.eq('event_id', showFilter);
|
||||
const { data, error } = await query;
|
||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
return NextResponse.json({ exhibitors: data || [] });
|
||||
}
|
||||
|
||||
// ── Style Profiles ──
|
||||
if (tab === 'style-profiles' || action === 'style-profiles') {
|
||||
const { data, error } = await supabase
|
||||
.from('author_style_profiles')
|
||||
.select('*')
|
||||
.order('last_updated', { ascending: false });
|
||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
return NextResponse.json({ profiles: data || [] });
|
||||
}
|
||||
|
||||
// ── Settings ──
|
||||
if (action === 'settings') {
|
||||
const { data } = await supabase
|
||||
.from('company_tracker_settings')
|
||||
.select('*')
|
||||
.limit(1)
|
||||
.single();
|
||||
return NextResponse.json({ settings: data || {} });
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Unknown action' }, { status: 400 });
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: err.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── POST ─────────────────────────────────────────────────────────────────────
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const supabase = await createClient();
|
||||
const body = await request.json();
|
||||
const { action } = body;
|
||||
|
||||
// ── Add / Update Event ──
|
||||
if (action === 'upsert_event') {
|
||||
const { id, ...eventData } = body;
|
||||
let result;
|
||||
if (id) {
|
||||
result = await supabase
|
||||
.from('event_calendar')
|
||||
.update({ ...eventData, updated_at: new Date().toISOString() })
|
||||
.eq('id', id)
|
||||
.select()
|
||||
.single();
|
||||
} else {
|
||||
result = await supabase
|
||||
.from('event_calendar')
|
||||
.insert(eventData)
|
||||
.select()
|
||||
.single();
|
||||
}
|
||||
if (result.error) return NextResponse.json({ error: result.error.message }, { status: 500 });
|
||||
return NextResponse.json({ event: result.data });
|
||||
}
|
||||
|
||||
// ── Toggle Engine Pause ──
|
||||
if (action === 'toggle_engine') {
|
||||
const { event_id, paused } = body;
|
||||
const { error } = await supabase
|
||||
.from('event_calendar')
|
||||
.update({ engine_paused: paused, updated_at: new Date().toISOString() })
|
||||
.eq('id', event_id);
|
||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
// ── Extract Ryan Salazar Style Profile ──
|
||||
if (action === 'extract_style') {
|
||||
const { event_name, author_username = 'ryansalazar' } = body;
|
||||
|
||||
// Fetch Ryan's articles mentioning this event
|
||||
const keywords = event_name.split(' ').filter((w: string) => w.length > 3);
|
||||
const searchTerm = keywords[0] || event_name;
|
||||
|
||||
const { data: articles } = await supabase
|
||||
.from('native_articles')
|
||||
.select('title, content, published_at')
|
||||
.eq('author_name', author_username)
|
||||
.ilike('content', `%${searchTerm}%`)
|
||||
.eq('status', 'published')
|
||||
.order('published_at', { ascending: false })
|
||||
.limit(10);
|
||||
|
||||
const styleGuide = await extractStyleProfile(articles || [], event_name);
|
||||
|
||||
if (styleGuide) {
|
||||
const { data: existing } = await supabase
|
||||
.from('author_style_profiles')
|
||||
.select('id')
|
||||
.eq('author_username', author_username)
|
||||
.eq('event_name', event_name)
|
||||
.single();
|
||||
|
||||
if (existing?.id) {
|
||||
await supabase
|
||||
.from('author_style_profiles')
|
||||
.update({
|
||||
style_guide_text: styleGuide,
|
||||
source_article_count: articles?.length || 0,
|
||||
last_updated: new Date().toISOString(),
|
||||
})
|
||||
.eq('id', existing.id);
|
||||
} else {
|
||||
await supabase.from('author_style_profiles').insert({
|
||||
author_username,
|
||||
event_type: 'trade_show',
|
||||
event_name,
|
||||
style_guide_text: styleGuide,
|
||||
source_article_count: articles?.length || 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
articleCount: articles?.length || 0,
|
||||
styleGuideGenerated: !!styleGuide,
|
||||
});
|
||||
}
|
||||
|
||||
// ── Generate Show Story ──
|
||||
if (action === 'generate_story') {
|
||||
const {
|
||||
event_id, story_type, company_name, source_content,
|
||||
preferred_pen_name, site_id = 1,
|
||||
} = body;
|
||||
|
||||
// Fetch event details
|
||||
const { data: event } = await supabase
|
||||
.from('event_calendar')
|
||||
.select('*')
|
||||
.eq('id', event_id)
|
||||
.single();
|
||||
|
||||
if (!event) return NextResponse.json({ error: 'Event not found' }, { status: 404 });
|
||||
|
||||
// Fetch style guide
|
||||
const { data: styleProfile } = await supabase
|
||||
.from('author_style_profiles')
|
||||
.select('style_guide_text')
|
||||
.eq('event_name', event.event_name)
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
// Get recent pen names to avoid repetition
|
||||
const { data: recentStories } = await supabase
|
||||
.from('auto_story_queue')
|
||||
.select('assigned_pen_name')
|
||||
.eq('event_id', event_id)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(6);
|
||||
|
||||
const recentNames = (recentStories || []).map((s: any) => s.assigned_pen_name).filter(Boolean);
|
||||
const penName = pickPenNameForShow(site_id, story_type, recentNames, preferred_pen_name);
|
||||
const penNameData = (site_id === 2 ? AV_PEN_NAMES : BB_PEN_NAMES).find(p => p.name === penName);
|
||||
const phase = getShowPhase(event);
|
||||
const siteName = site_id === 2 ? 'AV Beat' : 'Broadcast Beat';
|
||||
|
||||
const story = await generateShowStory({
|
||||
penName,
|
||||
penTitle: penNameData?.title || 'Correspondent',
|
||||
siteName,
|
||||
eventName: event.event_name,
|
||||
year: event.year,
|
||||
storyType: story_type,
|
||||
companyName: company_name,
|
||||
sourceContent: source_content,
|
||||
styleGuide: styleProfile?.style_guide_text,
|
||||
phase,
|
||||
});
|
||||
|
||||
// Insert into queue
|
||||
const { data: queued, error: queueError } = await supabase
|
||||
.from('auto_story_queue')
|
||||
.insert({
|
||||
company_name: company_name || event.event_name,
|
||||
story_type: story_type,
|
||||
assigned_pen_name: penName,
|
||||
site_id,
|
||||
event_id,
|
||||
show_phase: phase,
|
||||
show_story_type: story_type,
|
||||
source_show: event.event_name,
|
||||
status: 'generated',
|
||||
generated_title: story.title,
|
||||
generated_content: story.content,
|
||||
generated_excerpt: story.excerpt,
|
||||
seo_meta_description: story.metaDescription,
|
||||
quality_confidence: story.confidence,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (queueError) return NextResponse.json({ error: queueError.message }, { status: 500 });
|
||||
return NextResponse.json({ story: queued, penName, phase });
|
||||
}
|
||||
|
||||
// ── Add Exhibitor ──
|
||||
if (action === 'add_exhibitor') {
|
||||
const { company_name, event_id, booth_number, is_av_eligible } = body;
|
||||
|
||||
// Find or create tracked company
|
||||
const { data: existing } = await supabase
|
||||
.from('tracked_companies')
|
||||
.select('id')
|
||||
.eq('company_name', company_name)
|
||||
.single();
|
||||
|
||||
let companyId = existing?.id;
|
||||
if (!companyId) {
|
||||
const { data: newCompany } = await supabase
|
||||
.from('tracked_companies')
|
||||
.insert({ company_name, auto_story_enabled: true, site_scope: 'both' })
|
||||
.select('id')
|
||||
.single();
|
||||
companyId = newCompany?.id;
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('nab_exhibitors')
|
||||
.upsert({
|
||||
company_id: companyId,
|
||||
company_name,
|
||||
event_id,
|
||||
booth_number,
|
||||
is_av_eligible: is_av_eligible || false,
|
||||
}, { onConflict: 'company_name,event_id' })
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
return NextResponse.json({ exhibitor: data });
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Unknown action' }, { status: 400 });
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: err.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── PATCH ────────────────────────────────────────────────────────────────────
|
||||
export async function PATCH(request: NextRequest) {
|
||||
try {
|
||||
const supabase = await createClient();
|
||||
const body = await request.json();
|
||||
const { action, id } = body;
|
||||
|
||||
if (action === 'approve_story') {
|
||||
const { data: story } = await supabase
|
||||
.from('auto_story_queue')
|
||||
.select('*')
|
||||
.eq('id', id)
|
||||
.single();
|
||||
|
||||
if (!story) return NextResponse.json({ error: 'Story not found' }, { status: 404 });
|
||||
|
||||
const slug = (story.generated_title || 'show-story')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.trim()
|
||||
.slice(0, 80) + '-' + Date.now();
|
||||
|
||||
const { data: published } = await supabase
|
||||
.from('native_articles')
|
||||
.insert({
|
||||
slug,
|
||||
title: story.generated_title,
|
||||
excerpt: story.generated_excerpt,
|
||||
content: story.generated_content,
|
||||
author_name: story.assigned_pen_name,
|
||||
status: 'published',
|
||||
site_id: story.site_id,
|
||||
published_at: new Date().toISOString(),
|
||||
})
|
||||
.select('id')
|
||||
.single();
|
||||
|
||||
await supabase
|
||||
.from('auto_story_queue')
|
||||
.update({
|
||||
status: 'published',
|
||||
review_status: 'approved',
|
||||
published_article_id: published?.id,
|
||||
reviewed_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.eq('id', id);
|
||||
|
||||
// Queue translations for all 9 languages
|
||||
const languages = ['es', 'pt', 'fr', 'de', 'zh', 'ja', 'ar', 'hi'];
|
||||
const translationJobs = languages.map((lang, idx) => ({
|
||||
post_id: published?.id,
|
||||
post_type: 'native',
|
||||
language_code: lang,
|
||||
priority: idx < 2 ? 8 : idx < 4 ? 6 : 4, // es/pt first, then fr/de, then rest
|
||||
status: 'pending',
|
||||
}));
|
||||
|
||||
if (published?.id) {
|
||||
await supabase.from('translation_queue').insert(translationJobs);
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, articleId: published?.id });
|
||||
}
|
||||
|
||||
if (action === 'reject_story') {
|
||||
const { reason } = body;
|
||||
await supabase
|
||||
.from('auto_story_queue')
|
||||
.update({
|
||||
status: 'rejected',
|
||||
review_status: 'rejected',
|
||||
rejection_reason: reason,
|
||||
reviewed_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.eq('id', id);
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
if (action === 'update_event_dates') {
|
||||
const { event_id, start_date, end_date, story_engine_start_date, story_engine_end_date } = body;
|
||||
await supabase
|
||||
.from('event_calendar')
|
||||
.update({
|
||||
start_date, end_date, story_engine_start_date, story_engine_end_date,
|
||||
status: 'upcoming', updated_at: new Date().toISOString(),
|
||||
})
|
||||
.eq('id', event_id);
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Unknown action' }, { status: 400 });
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: err.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user