initial commit: rocket.new export of broadcastbeat

This commit is contained in:
Ryan Salazar
2026-05-07 16:39:17 +00:00
commit 70aa1ad46e
302 changed files with 60710 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
import { requireRmpAdmin } from '@/lib/accounting/rmpService';
export async function GET(request: NextRequest) {
const auth = await requireRmpAdmin();
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const supabase = await createClient();
const { searchParams } = new URL(request.url);
const status = searchParams.get('status') ?? 'active';
const search = searchParams.get('search') ?? '';
let query = supabase.from('rmp_clients').select('*').order('company_name');
if (status && status !== 'all') query = query.eq('status', status);
if (search) query = query.ilike('company_name', `%${search}%`);
const { data, error } = await query;
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json({ clients: data ?? [] });
}
export async function POST(request: NextRequest) {
const auth = await requireRmpAdmin();
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const supabase = await createClient();
const body = await request.json();
const { data, error } = await supabase.from('rmp_clients').insert({
company_name: body.company_name,
contact_name: body.contact_name,
contact_email: body.contact_email,
contact_phone: body.contact_phone,
billing_address: body.billing_address,
notes: body.notes,
status: 'active',
}).select().single();
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json({ client: data });
}

View File

@@ -0,0 +1,19 @@
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
import { requireRmpAdmin } from '@/lib/accounting/rmpService';
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const auth = await requireRmpAdmin();
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const { id } = await params;
const supabase = await createClient();
const { error } = await supabase.from('rmp_commissions').update({ paid: true, paid_date: new Date().toISOString().split('T')[0] }).eq('id', id);
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json({ ok: true });
}

View File

@@ -0,0 +1,26 @@
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
import { requireRmpAdmin } from '@/lib/accounting/rmpService';
export async function GET(request: NextRequest) {
const auth = await requireRmpAdmin();
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const supabase = await createClient();
const { searchParams } = new URL(request.url);
const year = parseInt(searchParams.get('year') ?? String(new Date().getFullYear()));
const staff_id = searchParams.get('staff_id');
let query = supabase
.from('rmp_commissions')
.select('*, rmp_sales_staff(full_name), rmp_invoices(invoice_number)')
.eq('year', year)
.order('created_at', { ascending: false });
if (staff_id) query = query.eq('staff_id', staff_id);
const { data, error } = await query;
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json({ commissions: data ?? [] });
}

View File

@@ -0,0 +1,39 @@
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
import { requireRmpAdmin } from '@/lib/accounting/rmpService';
export async function GET(request: NextRequest) {
const auth = await requireRmpAdmin();
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const supabase = await createClient();
const { searchParams } = new URL(request.url);
const year = parseInt(searchParams.get('year') ?? String(new Date().getFullYear()));
const { data: staff } = await supabase.from('rmp_sales_staff').select('id, full_name').eq('status', 'active');
const { data: tiers } = await supabase.from('rmp_commission_tiers').select('*').eq('effective_year', year).order('tier_order');
const { data: comms } = await supabase.from('rmp_commissions').select('*').eq('year', year);
const summary = (staff ?? []).map((s: any) => {
const staffComms = (comms ?? []).filter((c: any) => c.staff_id === s.id);
const staffTiers = (tiers ?? []).filter((t: any) => t.staff_id === s.id);
const ytd_sales_cents = staffComms.reduce((sum: number, c: any) => sum + (c.sale_amount_cents ?? 0), 0);
const earned_cents = staffComms.reduce((sum: number, c: any) => sum + (c.commission_amount_cents ?? 0), 0);
const outstanding_cents = staffComms.filter((c: any) => !c.paid).reduce((sum: number, c: any) => sum + (c.commission_amount_cents ?? 0), 0);
// Find current tier
let current_tier = 'Tier 1';
let next_threshold_cents = null;
for (const tier of staffTiers) {
if (!tier.threshold_cents || ytd_sales_cents < tier.threshold_cents) {
current_tier = `Tier ${tier.tier_order} (${(tier.rate * 100).toFixed(0)}%)`;
next_threshold_cents = tier.threshold_cents;
break;
}
}
return { staff_id: s.id, full_name: s.full_name, ytd_sales_cents, earned_cents, outstanding_cents, current_tier, next_threshold_cents };
});
return NextResponse.json({ summary });
}

View File

@@ -0,0 +1,88 @@
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
import { requireRmpAdmin } from '@/lib/accounting/rmpService';
export async function GET(request: NextRequest) {
const auth = await requireRmpAdmin();
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const supabase = await createClient();
const now = new Date();
const yearStart = `${now.getFullYear()}-01-01`;
const monthStart = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-01`;
// Revenue YTD (paid invoices)
const { data: revenueYtd } = await supabase
.from('rmp_invoices')
.select('amount_cents')
.eq('status', 'paid')
.gte('paid_date', yearStart);
const revenue_ytd = (revenueYtd ?? []).reduce((s: number, r: any) => s + (r.amount_cents ?? 0), 0);
// Expenses YTD
const { data: expensesYtd } = await supabase
.from('rmp_expenses')
.select('amount_cents')
.gte('expense_date', yearStart);
const expenses_ytd = (expensesYtd ?? []).reduce((s: number, r: any) => s + (r.amount_cents ?? 0), 0);
// Outstanding invoices
const { data: outstanding } = await supabase
.from('rmp_invoices')
.select('amount_cents')
.in('status', ['pending', 'overdue']);
const outstanding_total = (outstanding ?? []).reduce((s: number, r: any) => s + (r.amount_cents ?? 0), 0);
// Unreconciled transactions
const { count: unreconciled } = await supabase
.from('rmp_bank_transactions')
.select('*', { count: 'exact', head: true })
.eq('reconciled', false);
// Commissions outstanding
const { data: commOutstanding } = await supabase
.from('rmp_commissions')
.select('commission_amount_cents')
.eq('paid', false);
const commissions_outstanding = (commOutstanding ?? []).reduce((s: number, r: any) => s + (r.commission_amount_cents ?? 0), 0);
// Site revenue breakdown
const { data: siteInvoices } = await supabase
.from('rmp_invoices')
.select('site, amount_cents, status, paid_date')
.gte('created_at', yearStart);
const sites = ['broadcastbeat', 'avbeat', 'backlotbeat', 'broadcastengineering'];
const siteRevenue = sites.map(site => {
const siteData = (siteInvoices ?? []).filter((i: any) => i.site === site);
const revenue_ytd = siteData.filter((i: any) => i.status === 'paid').reduce((s: number, r: any) => s + (r.amount_cents ?? 0), 0);
const revenue_mtd = siteData.filter((i: any) => i.status === 'paid' && i.paid_date >= monthStart).reduce((s: number, r: any) => s + (r.amount_cents ?? 0), 0);
const outstanding = siteData.filter((i: any) => ['pending', 'overdue'].includes(i.status)).reduce((s: number, r: any) => s + (r.amount_cents ?? 0), 0);
return { site, revenue_ytd, revenue_mtd, outstanding, active_orders: 0 };
});
// Top clients
const { data: clientInvoices } = await supabase
.from('rmp_invoices')
.select('client_id, amount_cents, rmp_clients(company_name)')
.eq('status', 'paid')
.gte('paid_date', yearStart);
const clientMap: Record<string, any> = {};
for (const inv of (clientInvoices ?? [])) {
if (!inv.client_id) continue;
if (!clientMap[inv.client_id]) clientMap[inv.client_id] = { id: inv.client_id, company_name: (inv as any).rmp_clients?.company_name, revenue_ytd: 0, active_orders: 0 };
clientMap[inv.client_id].revenue_ytd += inv.amount_cents ?? 0;
}
const topClients = Object.values(clientMap).sort((a: any, b: any) => b.revenue_ytd - a.revenue_ytd).slice(0, 5);
return NextResponse.json({
stats: { revenue_ytd, expenses_ytd, net_profit: revenue_ytd - expenses_ytd, outstanding: outstanding_total, unreconciled: unreconciled ?? 0, commissions_outstanding },
siteRevenue,
topClients,
});
}

View File

@@ -0,0 +1,42 @@
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
import { requireRmpAdmin } from '@/lib/accounting/rmpService';
export async function GET(request: NextRequest) {
const auth = await requireRmpAdmin();
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const supabase = await createClient();
const { searchParams } = new URL(request.url);
const type = searchParams.get('type');
const year = searchParams.get('year');
let query = supabase.from('rmp_documents').select('*').order('uploaded_at', { ascending: false });
if (type) query = query.eq('document_type', type);
if (year) query = query.eq('year', parseInt(year));
const { data, error } = await query;
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json({ documents: data ?? [] });
}
export async function POST(request: NextRequest) {
const auth = await requireRmpAdmin();
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const supabase = await createClient();
const body = await request.json();
const { data, error } = await supabase.from('rmp_documents').insert({
document_type: body.document_type,
title: body.title,
year: body.year || null,
person: body.person || null,
file_url: body.file_url || null,
notes: body.notes || null,
}).select().single();
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json({ document: data });
}

View File

@@ -0,0 +1,44 @@
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
import { requireRmpAdmin } from '@/lib/accounting/rmpService';
export async function GET(request: NextRequest) {
const auth = await requireRmpAdmin();
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const supabase = await createClient();
const { searchParams } = new URL(request.url);
const category = searchParams.get('category');
const reconciled = searchParams.get('reconciled');
let query = supabase.from('rmp_expenses').select('*').order('expense_date', { ascending: false });
if (category) query = query.eq('category', category);
if (reconciled !== null && reconciled !== '') query = query.eq('reconciled', reconciled === 'true');
const { data, error } = await query;
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json({ expenses: data ?? [] });
}
export async function POST(request: NextRequest) {
const auth = await requireRmpAdmin();
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const supabase = await createClient();
const body = await request.json();
const { data, error } = await supabase.from('rmp_expenses').insert({
vendor: body.vendor || null,
amount_cents: body.amount_cents,
currency: body.currency ?? 'USD',
category: body.category,
description: body.description || null,
expense_date: body.expense_date,
receipt_source: body.receipt_source ?? 'manual',
reconciled: false,
}).select().single();
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json({ expense: data });
}

View File

@@ -0,0 +1,29 @@
import { NextRequest, NextResponse } from 'next/server';
import { requireRmpAdmin, markInvoicePaid } from '@/lib/accounting/rmpService';
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const auth = await requireRmpAdmin();
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const { id } = await params;
const body = await request.json();
const result = await markInvoicePaid(id, body.method, body.paid_date, body.wire_reference, body.stripe_payment_id);
if (!result.ok) return NextResponse.json({ error: result.error }, { status: 400 });
// Notify
try {
await fetch(`${process.env.NEXT_PUBLIC_SITE_URL}/api/accounting/notify-payment`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ invoice_id: id }),
});
} catch {}
return NextResponse.json({ ok: true });
}

View File

@@ -0,0 +1,51 @@
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
import { requireRmpAdmin, generateInvoiceNumber } from '@/lib/accounting/rmpService';
export async function GET(request: NextRequest) {
const auth = await requireRmpAdmin();
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const supabase = await createClient();
const { searchParams } = new URL(request.url);
const status = searchParams.get('status');
let query = supabase
.from('rmp_invoices')
.select('*, rmp_clients(company_name), rmp_orders(internal_order_number), rmp_sales_staff(full_name)')
.order('created_at', { ascending: false });
if (status) query = query.eq('status', status);
const { data, error } = await query;
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json({ invoices: data ?? [] });
}
export async function POST(request: NextRequest) {
const auth = await requireRmpAdmin();
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const supabase = await createClient();
const body = await request.json();
const invoice_number = await generateInvoiceNumber();
const { data, error } = await supabase.from('rmp_invoices').insert({
order_id: body.order_id || null,
client_id: body.client_id || null,
staff_id: body.staff_id || null,
invoice_number,
site: body.site,
amount_cents: body.amount_cents,
currency: body.currency ?? 'USD',
status: 'pending',
issue_date: body.issue_date || new Date().toISOString().split('T')[0],
due_date: body.due_date || null,
is_staff_sale: body.is_staff_sale ?? false,
notes: body.notes || null,
}).select().single();
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json({ invoice: data });
}

View File

@@ -0,0 +1,38 @@
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
export async function POST(request: NextRequest) {
const supabase = await createClient();
const body = await request.json();
const { invoice_id } = body;
const notifyEmail = process.env.NOTIFY_EMAIL;
const notifyPhone = process.env.NOTIFY_PHONE;
let message = 'Payment received';
if (invoice_id) {
const { data: inv } = await supabase.from('rmp_invoices').select('invoice_number, amount_cents').eq('id', invoice_id).single();
if (inv) message = `Payment received for invoice ${inv.invoice_number}: $${((inv.amount_cents ?? 0) / 100).toFixed(2)}`;
}
// Log notification
await supabase.from('rmp_notifications').insert({ type: 'payment_notify', message, invoice_id: invoice_id ?? null });
// Email notification (stub — SMTP configured)
if (notifyEmail) {
try {
await fetch(`${process.env.NEXT_PUBLIC_SITE_URL}/api/marketplace/send-email`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ to: notifyEmail, subject: 'RMP Payment Received', text: message }),
});
} catch {}
}
// SMS via Twilio — stub until credentials added
if (notifyPhone && process.env.TWILIO_ACCOUNT_SID) {
// TODO: Add Twilio SMS when credentials are configured
}
return NextResponse.json({ ok: true, message });
}

View File

@@ -0,0 +1,59 @@
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
import { requireRmpAdmin, generateOrderNumber } from '@/lib/accounting/rmpService';
export async function GET(request: NextRequest) {
const auth = await requireRmpAdmin();
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const supabase = await createClient();
const { searchParams } = new URL(request.url);
const site = searchParams.get('site');
const status = searchParams.get('status');
const client = searchParams.get('client');
let query = supabase
.from('rmp_orders')
.select('*, rmp_clients(company_name), rmp_products(name), rmp_sales_staff(full_name)')
.order('created_at', { ascending: false });
if (site) query = query.eq('site', site);
if (status) query = query.eq('status', status);
if (client) query = query.ilike('rmp_clients.company_name', `%${client}%`);
const { data, error } = await query;
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json({ orders: data ?? [] });
}
export async function POST(request: NextRequest) {
const auth = await requireRmpAdmin();
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const supabase = await createClient();
const body = await request.json();
const internal_order_number = await generateOrderNumber();
const { data, error } = await supabase.from('rmp_orders').insert({
client_id: body.client_id || null,
staff_id: body.staff_id || null,
po_number: body.po_number || null,
internal_order_number,
site: body.site,
product_id: body.product_id || null,
ad_unit: body.ad_unit || null,
description: body.description || null,
start_date: body.start_date || null,
end_date: body.end_date || null,
total_amount_cents: body.total_amount_cents || null,
currency: body.currency ?? 'USD',
invoicing_schedule: body.invoicing_schedule ?? 'single',
next_invoice_date: body.next_invoice_date || null,
notes: body.notes || null,
status: 'active',
}).select().single();
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json({ order: data });
}

View File

@@ -0,0 +1,20 @@
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
import { requireRmpAdmin } from '@/lib/accounting/rmpService';
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const auth = await requireRmpAdmin();
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const { id } = await params;
const supabase = await createClient();
const body = await request.json();
const { error } = await supabase.from('rmp_products').update(body).eq('id', id);
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json({ ok: true });
}

View File

@@ -0,0 +1,42 @@
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
import { requireRmpAdmin } from '@/lib/accounting/rmpService';
export async function GET(request: NextRequest) {
const auth = await requireRmpAdmin();
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const supabase = await createClient();
const { searchParams } = new URL(request.url);
const site = searchParams.get('site');
let query = supabase.from('rmp_products').select('*').order('site').order('sort_order');
if (site) query = query.eq('site', site);
const { data, error } = await query;
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json({ products: data ?? [] });
}
export async function POST(request: NextRequest) {
const auth = await requireRmpAdmin();
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const supabase = await createClient();
const body = await request.json();
const { data, error } = await supabase.from('rmp_products').insert({
site: body.site,
product_type: body.product_type,
name: body.name,
description: body.description || null,
dimensions: body.dimensions || null,
duration_days: body.duration_days || null,
sort_order: body.sort_order ?? 0,
active: true,
}).select().single();
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json({ product: data });
}

View File

@@ -0,0 +1,92 @@
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
import { requireRmpAdmin } from '@/lib/accounting/rmpService';
export async function GET(request: NextRequest) {
const auth = await requireRmpAdmin();
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const supabase = await createClient();
const { data: transactions, error } = await supabase
.from('rmp_bank_transactions')
.select('*')
.order('transaction_date', { ascending: false })
.limit(200);
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
let matched = (transactions ?? []).filter((t: any) => t.reconciled).length;
let unmatched = (transactions ?? []).filter((t: any) => !t.reconciled).length;
return NextResponse.json({ transactions: transactions ?? [], summary: { matched, unmatched } });
}
export async function POST(request: NextRequest) {
const auth = await requireRmpAdmin();
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const supabase = await createClient();
const body = await request.json();
const csv: string = body.csv ?? '';
const lines = csv.split('\n').filter(Boolean);
const headers = lines[0]?.split(',').map((h: string) => h.trim().toLowerCase().replace(/"/g, '')) ?? [];
const rows = lines.slice(1).map((line: string) => {
const vals = line.split(',').map((v: string) => v.trim().replace(/"/g, ''));
const row: Record<string, string> = {};
headers.forEach((h: string, i: number) => { row[h] = vals[i] ?? ''; });
return row;
});
let matched = 0;
let unmatched = 0;
for (const row of rows) {
const date = row['date'] || row['transaction date'] || row['trans date'];
const desc = row['description'] || row['memo'] || row['payee'];
const amountStr = row['amount'] || row['debit'] || row['credit'];
const amount = parseFloat(amountStr?.replace(/[$,]/g, '') ?? '0');
if (!date || isNaN(amount)) continue;
const amount_cents = Math.round(Math.abs(amount) * 100);
const type = amount >= 0 ? 'credit' : 'debit';
// Try to auto-match to invoice
let matched_invoice_id = null;
if (type === 'credit') {
const { data: invoices } = await supabase
.from('rmp_invoices')
.select('id')
.eq('amount_cents', amount_cents)
.eq('status', 'pending')
.gte('due_date', new Date(new Date(date).getTime() - 3 * 86400000).toISOString().split('T')[0])
.lte('due_date', new Date(new Date(date).getTime() + 3 * 86400000).toISOString().split('T')[0])
.limit(1);
if (invoices?.[0]) {
matched_invoice_id = invoices[0].id;
matched++;
} else {
unmatched++;
}
}
await supabase.from('rmp_bank_transactions').insert({
transaction_date: date,
description: desc,
amount_cents: type === 'debit' ? -amount_cents : amount_cents,
type,
matched_invoice_id,
reconciled: !!matched_invoice_id,
raw_csv_row: row,
});
// If matched, mark invoice paid
if (matched_invoice_id) {
await supabase.from('rmp_invoices').update({ status: 'paid', paid_date: date, payment_method: 'wire' }).eq('id', matched_invoice_id);
}
}
return NextResponse.json({ ok: true, summary: { matched, unmatched } });
}

View File

@@ -0,0 +1,49 @@
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
import { requireRmpAdmin } from '@/lib/accounting/rmpService';
export async function GET(request: NextRequest) {
const auth = await requireRmpAdmin();
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const supabase = await createClient();
const { searchParams } = new URL(request.url);
const start = searchParams.get('start') ?? `${new Date().getFullYear()}-01-01`;
const end = searchParams.get('end') ?? new Date().toISOString().split('T')[0];
const [invoicesRes, expensesRes] = await Promise.all([
supabase.from('rmp_invoices').select('site, amount_cents, client_id, rmp_clients(company_name)').eq('status', 'paid').gte('paid_date', start).lte('paid_date', end),
supabase.from('rmp_expenses').select('category, amount_cents').gte('expense_date', start).lte('expense_date', end),
]);
const invoices = invoicesRes.data ?? [];
const expenses = expensesRes.data ?? [];
const total_revenue_cents = invoices.reduce((s: number, i: any) => s + (i.amount_cents ?? 0), 0);
const total_expenses_cents = expenses.reduce((s: number, e: any) => s + (e.amount_cents ?? 0), 0);
// By site
const siteMap: Record<string, number> = {};
for (const inv of invoices) {
siteMap[inv.site] = (siteMap[inv.site] ?? 0) + (inv.amount_cents ?? 0);
}
const by_site = Object.entries(siteMap).map(([site, revenue_cents]) => ({ site, revenue_cents })).sort((a, b) => b.revenue_cents - a.revenue_cents);
// Top clients
const clientMap: Record<string, any> = {};
for (const inv of invoices) {
if (!inv.client_id) continue;
if (!clientMap[inv.client_id]) clientMap[inv.client_id] = { id: inv.client_id, company_name: (inv as any).rmp_clients?.company_name, revenue_cents: 0 };
clientMap[inv.client_id].revenue_cents += inv.amount_cents ?? 0;
}
const top_clients = Object.values(clientMap).sort((a: any, b: any) => b.revenue_cents - a.revenue_cents).slice(0, 10);
// By expense category
const catMap: Record<string, number> = {};
for (const exp of expenses) {
catMap[exp.category ?? 'other'] = (catMap[exp.category ?? 'other'] ?? 0) + (exp.amount_cents ?? 0);
}
const by_category = Object.entries(catMap).map(([category, amount_cents]) => ({ category, amount_cents })).sort((a, b) => b.amount_cents - a.amount_cents);
return NextResponse.json({ total_revenue_cents, total_expenses_cents, net_profit_cents: total_revenue_cents - total_expenses_cents, by_site, top_clients, by_category });
}

View File

@@ -0,0 +1,66 @@
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
import { requireRmpAdmin } from '@/lib/accounting/rmpService';
export async function GET(request: NextRequest) {
const auth = await requireRmpAdmin();
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const supabase = await createClient();
const { data: staff, error } = await supabase
.from('rmp_sales_staff')
.select('*')
.order('full_name');
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
// Attach YTD commission data
const year = new Date().getFullYear();
const enriched = await Promise.all((staff ?? []).map(async (s: any) => {
const { data: comms } = await supabase
.from('rmp_commissions')
.select('commission_amount_cents, paid')
.eq('staff_id', s.id)
.eq('year', year);
const commission_ytd = (comms ?? []).reduce((sum: number, c: any) => sum + (c.commission_amount_cents ?? 0), 0);
const commission_outstanding = (comms ?? []).filter((c: any) => !c.paid).reduce((sum: number, c: any) => sum + (c.commission_amount_cents ?? 0), 0);
return { ...s, commission_ytd, commission_outstanding };
}));
return NextResponse.json({ staff: enriched });
}
export async function POST(request: NextRequest) {
const auth = await requireRmpAdmin();
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const supabase = await createClient();
const body = await request.json();
const { data: staffMember, error } = await supabase.from('rmp_sales_staff').insert({
full_name: body.full_name,
email: body.email || null,
phone: body.phone || null,
notes: body.notes || null,
status: 'active',
}).select().single();
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
// Insert commission tiers
if (body.tiers?.length > 0) {
await supabase.from('rmp_commission_tiers').insert(
body.tiers.map((t: any) => ({
staff_id: staffMember.id,
effective_year: t.effective_year,
tier_order: t.tier_order,
threshold_cents: t.threshold_cents,
rate: t.rate,
}))
);
}
return NextResponse.json({ staff: staffMember });
}

View File

@@ -0,0 +1,51 @@
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
// Stub — Stripe credentials will be added later
export async function POST(request: NextRequest) {
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET_RMP;
if (!webhookSecret) {
return NextResponse.json({ error: 'Stripe not configured' }, { status: 503 });
}
// TODO: Verify Stripe webhook signature when STRIPE_WEBHOOK_SECRET_RMP is set
// const sig = request.headers.get('stripe-signature');
// const event = stripe.webhooks.constructEvent(body, sig, webhookSecret);
const body = await request.json();
const eventType = body?.type;
if (eventType === 'payment_intent.succeeded') {
const supabase = await createClient();
const stripeId = body?.data?.object?.id;
const amountCents = body?.data?.object?.amount;
if (stripeId && amountCents) {
const { data: invoice } = await supabase
.from('rmp_invoices')
.select('id')
.eq('amount_cents', amountCents)
.eq('status', 'pending')
.limit(1)
.single();
if (invoice) {
await supabase.from('rmp_invoices').update({
status: 'paid',
payment_method: 'stripe',
stripe_payment_id: stripeId,
paid_date: new Date().toISOString().split('T')[0],
}).eq('id', invoice.id);
await supabase.from('rmp_notifications').insert({
type: 'stripe_payment',
message: `Stripe payment received: ${stripeId}`,
invoice_id: invoice.id,
});
}
}
}
return NextResponse.json({ received: true });
}