Remove influencer-specific features (campaigns, TikTok, gifts, shadow ledger, contracts, briefings) and consolidate into a clean ERP foundation with core bookkeeping, invoicing, receipts, tax reporting, and calendar functionality. Reorganize database migrations into a clean numbered sequence. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
114 lines
3.3 KiB
TypeScript
114 lines
3.3 KiB
TypeScript
import { createClient } from '@supabase/supabase-js'
|
|
import { NextResponse } from 'next/server'
|
|
import { generateCalendarFeed } from '@/lib/calendar/ics-generator'
|
|
|
|
/**
|
|
* GET /api/calendar/feed/[token]
|
|
* Returns an ICS calendar feed for the given token
|
|
* No authentication required - the token IS the authentication
|
|
*/
|
|
export async function GET(
|
|
request: Request,
|
|
{ params }: { params: Promise<{ token: string }> }
|
|
) {
|
|
const { token } = await params
|
|
|
|
// Validate token format (UUID)
|
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
if (!uuidRegex.test(token)) {
|
|
return new NextResponse('Invalid token', { status: 400 })
|
|
}
|
|
|
|
// Create service client (no user auth required)
|
|
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
|
|
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY
|
|
|
|
if (!supabaseUrl || !supabaseServiceKey) {
|
|
return new NextResponse('Server configuration error', { status: 500 })
|
|
}
|
|
|
|
const supabase = createClient(supabaseUrl, supabaseServiceKey)
|
|
|
|
// Fetch feed settings by token
|
|
const { data: feed, error: feedError } = await supabase
|
|
.from('calendar_feeds')
|
|
.select('*')
|
|
.eq('feed_token', token)
|
|
.eq('is_active', true)
|
|
.single()
|
|
|
|
if (feedError || !feed) {
|
|
return new NextResponse('Feed not found or inactive', { status: 404 })
|
|
}
|
|
|
|
// Update access tracking
|
|
await supabase
|
|
.from('calendar_feeds')
|
|
.update({
|
|
last_accessed_at: new Date().toISOString(),
|
|
access_count: feed.access_count + 1,
|
|
})
|
|
.eq('id', feed.id)
|
|
|
|
// Calculate date range: 3 months back, 12 months forward
|
|
const now = new Date()
|
|
const startDate = new Date(now)
|
|
startDate.setMonth(startDate.getMonth() - 3)
|
|
const endDate = new Date(now)
|
|
endDate.setMonth(endDate.getMonth() + 12)
|
|
|
|
const startStr = startDate.toISOString().split('T')[0]
|
|
const endStr = endDate.toISOString().split('T')[0]
|
|
|
|
// Fetch relevant data based on feed options
|
|
const [deadlinesResult, invoicesResult] = await Promise.all([
|
|
// Deadlines
|
|
feed.include_tax_deadlines
|
|
? supabase
|
|
.from('deadlines')
|
|
.select('*')
|
|
.eq('user_id', feed.user_id)
|
|
.gte('due_date', startStr)
|
|
.lte('due_date', endStr)
|
|
.order('due_date')
|
|
: { data: [] },
|
|
|
|
// Invoices
|
|
feed.include_invoices
|
|
? supabase
|
|
.from('invoices')
|
|
.select('*, customer:customers(*)')
|
|
.eq('user_id', feed.user_id)
|
|
.gte('due_date', startStr)
|
|
.lte('due_date', endStr)
|
|
.order('due_date')
|
|
: { data: [] },
|
|
])
|
|
|
|
try {
|
|
const icsContent = await generateCalendarFeed(
|
|
{
|
|
deadlines: deadlinesResult.data || [],
|
|
invoices: invoicesResult.data || [],
|
|
},
|
|
{
|
|
includeTaxDeadlines: feed.include_tax_deadlines,
|
|
includeInvoices: feed.include_invoices,
|
|
}
|
|
)
|
|
|
|
return new NextResponse(icsContent, {
|
|
headers: {
|
|
'Content-Type': 'text/calendar; charset=utf-8',
|
|
'Content-Disposition': 'attachment; filename="erp-base.ics"',
|
|
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
'Pragma': 'no-cache',
|
|
'Expires': '0',
|
|
},
|
|
})
|
|
} catch (error) {
|
|
console.error('Error generating ICS feed:', error)
|
|
return new NextResponse('Failed to generate calendar feed', { status: 500 })
|
|
}
|
|
}
|