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,276 @@
'use client';
import React, { useState, useEffect, useCallback, Suspense } from 'react';
import Link from 'next/link';
import { useAuth } from '@/contexts/AuthContext';
import { useRouter, useSearchParams } from 'next/navigation';
const SITES = ['broadcastbeat','avbeat','backlotbeat','broadcastengineering'];
const STATUSES = ['active','completed','cancelled'];
const SCHEDULES = ['single','monthly','quarterly'];
function fmt(cents: number) { return '$' + (cents / 100).toLocaleString('en-US', { minimumFractionDigits: 2 }); }
function OrdersContent() {
const { user, loading } = useAuth();
const router = useRouter();
const sp = useSearchParams();
const [orders, setOrders] = useState<any[]>([]);
const [clients, setClients] = useState<any[]>([]);
const [staff, setStaff] = useState<any[]>([]);
const [products, setProducts] = useState<any[]>([]);
const [loadingData, setLoadingData] = useState(true);
const [showForm, setShowForm] = useState(false);
const [saving, setSaving] = useState(false);
const [filterSite, setFilterSite] = useState(sp.get('site') ?? '');
const [filterStatus, setFilterStatus] = useState(sp.get('status') ?? '');
const [filterClient, setFilterClient] = useState('');
const [errors, setErrors] = useState<Record<string, string>>({});
const [successMsg, setSuccessMsg] = useState('');
const [form, setForm] = useState({
client_id: '', site: '', product_id: '', ad_unit: '', description: '',
start_date: '', end_date: '', total_amount_cents: '', currency: 'USD',
invoicing_schedule: 'single', next_invoice_date: '', staff_id: '', po_number: '', notes: '',
});
useEffect(() => { if (!loading && !user) router.push('/login'); }, [user, loading, router]);
const load = useCallback(async () => {
setLoadingData(true);
const params = new URLSearchParams();
if (filterSite) params.set('site', filterSite);
if (filterStatus) params.set('status', filterStatus);
if (filterClient) params.set('client', filterClient);
const [ordersRes, clientsRes, staffRes] = await Promise.all([
fetch(`/api/accounting/orders?${params}`),
fetch('/api/accounting/clients?status=all'),
fetch('/api/accounting/staff'),
]);
const [od, cd, sd] = await Promise.all([ordersRes.json(), clientsRes.json(), staffRes.json()]);
setOrders(od.orders ?? []);
setClients(cd.clients ?? []);
setStaff(sd.staff ?? []);
setLoadingData(false);
}, [filterSite, filterStatus, filterClient]);
useEffect(() => { if (user) load(); }, [user, load]);
useEffect(() => {
if (!form.site) return;
fetch(`/api/accounting/products?site=${form.site}`)
.then(r => r.json()).then(d => setProducts(d.products ?? []));
}, [form.site]);
function handleChange(key: string, value: string) {
setForm(p => ({ ...p, [key]: value }));
if (errors[key]) setErrors(p => { const n = { ...p }; delete n[key]; return n; });
}
function validate() {
const errs: Record<string, string> = {};
if (!form.client_id) errs.client_id = 'Client is required.';
if (!form.site) errs.site = 'Site is required.';
if (form.total_amount_cents && isNaN(parseFloat(form.total_amount_cents))) errs.total_amount_cents = 'Enter a valid amount.';
if (form.total_amount_cents && parseFloat(form.total_amount_cents) < 0) errs.total_amount_cents = 'Amount must be positive.';
if (form.start_date && form.end_date && form.end_date < form.start_date) errs.end_date = 'End date must be after start date.';
return errs;
}
async function handleCreate(e: React.FormEvent) {
e.preventDefault();
const errs = validate();
if (Object.keys(errs).length > 0) { setErrors(errs); return; }
setSaving(true);
try {
const body = { ...form, total_amount_cents: form.total_amount_cents ? Math.round(parseFloat(form.total_amount_cents) * 100) : null };
const res = await fetch('/api/accounting/orders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
if (!res.ok) throw new Error('Failed to create order');
const clientName = clients.find((c: any) => c.id === form.client_id)?.company_name ?? 'Order';
setShowForm(false);
setErrors({});
setForm({ client_id: '', site: '', product_id: '', ad_unit: '', description: '', start_date: '', end_date: '', total_amount_cents: '', currency: 'USD', invoicing_schedule: 'single', next_invoice_date: '', staff_id: '', po_number: '', notes: '' });
setSuccessMsg(`Order for "${clientName}" created successfully.`);
setTimeout(() => setSuccessMsg(''), 4000);
load();
} catch {
setErrors({ _form: 'Something went wrong. Please try again.' });
} finally {
setSaving(false);
}
}
function handleCancel() {
setShowForm(false);
setErrors({});
setForm({ client_id: '', site: '', product_id: '', ad_unit: '', description: '', start_date: '', end_date: '', total_amount_cents: '', currency: 'USD', invoicing_schedule: 'single', next_invoice_date: '', staff_id: '', po_number: '', notes: '' });
}
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null;
return (
<div className="min-h-screen bg-[#0a0a0a] text-white p-6">
<div className="max-w-7xl mx-auto space-y-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold">Orders</h1>
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link>
</div>
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500">+ New Order</button>
</div>
{/* Success Toast */}
{successMsg && (
<div className="flex items-center gap-2 px-4 py-2.5 bg-green-500/10 border border-green-500/30 rounded-lg text-green-400 text-xs">
<svg className="w-4 h-4 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /></svg>
{successMsg}
</div>
)}
{/* Filters */}
<div className="flex gap-2 flex-wrap items-center">
<select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Sites</option>
{SITES.map(s => <option key={s} value={s}>{s}</option>)}
</select>
<select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Statuses</option>
{STATUSES.map(s => <option key={s} value={s}>{s}</option>)}
</select>
<input value={filterClient} onChange={e => setFilterClient(e.target.value)} placeholder="Filter by client…" className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-white placeholder-[#555] focus:outline-none focus:border-[#3b82f6]" />
</div>
{/* New Order Form */}
{showForm && (
<form onSubmit={handleCreate} noValidate className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-3">
<h3 className="text-sm font-semibold">New Order</h3>
{errors._form && (
<div className="flex items-center gap-2 px-3 py-2 bg-red-500/10 border border-red-500/30 rounded text-red-400 text-xs">
<svg className="w-3.5 h-3.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
{errors._form}
</div>
)}
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
<div>
<label className="text-xs text-[#888] block mb-1">Client <span className="text-red-400">*</span></label>
<select value={form.client_id} onChange={e => handleChange('client_id', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#3b82f6] ${errors.client_id ? 'border-red-500/60' : 'border-[#252525]'}`}>
<option value="">Select client</option>
{clients.map((c: any) => <option key={c.id} value={c.id}>{c.company_name}</option>)}
</select>
{errors.client_id && <p className="mt-1 text-xs text-red-400">{errors.client_id}</p>}
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Site <span className="text-red-400">*</span></label>
<select value={form.site} onChange={e => handleChange('site', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#3b82f6] ${errors.site ? 'border-red-500/60' : 'border-[#252525]'}`}>
<option value="">Select site</option>
{SITES.map(s => <option key={s} value={s}>{s}</option>)}
</select>
{errors.site && <p className="mt-1 text-xs text-red-400">{errors.site}</p>}
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Product</label>
<select value={form.product_id} onChange={e => handleChange('product_id', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]">
<option value="">Select product</option>
{products.map((p: any) => <option key={p.id} value={p.id}>{p.name}</option>)}
</select>
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Ad Unit</label>
<input value={form.ad_unit} onChange={e => handleChange('ad_unit', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]" />
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Start Date</label>
<input type="date" value={form.start_date} onChange={e => handleChange('start_date', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]" />
</div>
<div>
<label className="text-xs text-[#888] block mb-1">End Date</label>
<input type="date" value={form.end_date} onChange={e => handleChange('end_date', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#3b82f6] ${errors.end_date ? 'border-red-500/60' : 'border-[#252525]'}`} />
{errors.end_date && <p className="mt-1 text-xs text-red-400">{errors.end_date}</p>}
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Total Amount ($)</label>
<input type="number" step="0.01" value={form.total_amount_cents} onChange={e => handleChange('total_amount_cents', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#3b82f6] ${errors.total_amount_cents ? 'border-red-500/60' : 'border-[#252525]'}`} />
{errors.total_amount_cents && <p className="mt-1 text-xs text-red-400">{errors.total_amount_cents}</p>}
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Invoicing Schedule</label>
<select value={form.invoicing_schedule} onChange={e => handleChange('invoicing_schedule', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]">
{SCHEDULES.map(s => <option key={s} value={s}>{s}</option>)}
</select>
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Next Invoice Date</label>
<input type="date" value={form.next_invoice_date} onChange={e => handleChange('next_invoice_date', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]" />
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Salesperson</label>
<select value={form.staff_id} onChange={e => handleChange('staff_id', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]">
<option value="">None</option>
{staff.map((s: any) => <option key={s.id} value={s.id}>{s.full_name}</option>)}
</select>
</div>
<div>
<label className="text-xs text-[#888] block mb-1">PO Number</label>
<input value={form.po_number} onChange={e => handleChange('po_number', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]" />
</div>
<div className="col-span-2 md:col-span-3">
<label className="text-xs text-[#888] block mb-1">Notes</label>
<textarea value={form.notes} onChange={e => handleChange('notes', e.target.value)} rows={2} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]" />
</div>
</div>
<div className="flex gap-2">
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Create Order'}</button>
<button type="button" onClick={handleCancel} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white">Cancel</button>
</div>
</form>
)}
{/* Table */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[#1a1a1a]">
{['Client','PO#','Order#','Site','Product','Ad Unit','Dates','Amount','Status',''].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium whitespace-nowrap">{h}</th>
))}
</tr>
</thead>
<tbody>
{loadingData ? (
<tr><td colSpan={10} className="px-4 py-8 text-center text-[#555] text-xs">Loading</td></tr>
) : orders.length === 0 ? (
<tr><td colSpan={10} className="px-4 py-8 text-center text-[#555] text-xs">No orders found</td></tr>
) : orders.map((o: any) => (
<tr key={o.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]">
<td className="px-3 py-2 text-white text-xs">{o.rmp_clients?.company_name ?? '—'}</td>
<td className="px-3 py-2 text-[#888] text-xs">{o.po_number ?? '—'}</td>
<td className="px-3 py-2 text-[#3b82f6] text-xs font-mono">{o.internal_order_number}</td>
<td className="px-3 py-2 text-[#888] text-xs capitalize">{o.site}</td>
<td className="px-3 py-2 text-[#ccc] text-xs">{o.rmp_products?.name ?? o.ad_unit ?? '—'}</td>
<td className="px-3 py-2 text-[#888] text-xs">{o.ad_unit ?? '—'}</td>
<td className="px-3 py-2 text-[#888] text-xs whitespace-nowrap">{o.start_date} {o.end_date ?? '∞'}</td>
<td className="px-3 py-2 text-green-400 text-xs">{o.total_amount_cents ? fmt(o.total_amount_cents) : '—'}</td>
<td className="px-3 py-2">
<span className={`px-2 py-0.5 rounded-full text-xs ${o.status === 'active' ? 'bg-green-400/10 text-green-400' : o.status === 'cancelled' ? 'bg-red-400/10 text-red-400' : 'bg-[#1a1a1a] text-[#555]'}`}>{o.status}</span>
</td>
<td className="px-3 py-2">
<Link href={`/admin/accounting/orders/${o.id}`} className="text-xs text-[#3b82f6] hover:underline">View</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}
export default function OrdersPage() {
return (
<Suspense fallback={<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /></div>}>
<OrdersContent />
</Suspense>
);
}