From aa33b12f8c21612083749d6166a83b4e1d407945 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Sat, 21 Mar 2026 17:47:41 -0700 Subject: [PATCH] =?UTF-8?q?feat(admin-web):=20Phase=201=20=E2=80=94=20orga?= =?UTF-8?q?nizations,=20support,=20AI=20budgets,=20waitlist=20pages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/app/(dashboard)/ai-budgets/page.tsx | 462 ++++++++++++++++++ .../app/(dashboard)/organizations/page.tsx | 409 ++++++++++++++++ .../src/app/(dashboard)/support/page.tsx | 371 ++++++++++++++ .../src/app/(dashboard)/waitlist/page.tsx | 356 ++++++++++++++ .../src/app/api/ai-budgets/[...path]/route.ts | 56 +++ .../src/app/api/orgs/[...path]/route.ts | 59 +++ .../src/app/api/support/[...path]/route.ts | 56 +++ .../src/app/api/waitlist/[...path]/route.ts | 56 +++ .../admin-web/src/components/sidebar-nav.tsx | 8 + 9 files changed, 1833 insertions(+) create mode 100644 dashboards/admin-web/src/app/(dashboard)/ai-budgets/page.tsx create mode 100644 dashboards/admin-web/src/app/(dashboard)/organizations/page.tsx create mode 100644 dashboards/admin-web/src/app/(dashboard)/support/page.tsx create mode 100644 dashboards/admin-web/src/app/(dashboard)/waitlist/page.tsx create mode 100644 dashboards/admin-web/src/app/api/ai-budgets/[...path]/route.ts create mode 100644 dashboards/admin-web/src/app/api/orgs/[...path]/route.ts create mode 100644 dashboards/admin-web/src/app/api/support/[...path]/route.ts create mode 100644 dashboards/admin-web/src/app/api/waitlist/[...path]/route.ts diff --git a/dashboards/admin-web/src/app/(dashboard)/ai-budgets/page.tsx b/dashboards/admin-web/src/app/(dashboard)/ai-budgets/page.tsx new file mode 100644 index 00000000..8fa9f27c --- /dev/null +++ b/dashboards/admin-web/src/app/(dashboard)/ai-budgets/page.tsx @@ -0,0 +1,462 @@ +'use client'; + +import { useState, useEffect, useCallback } from 'react'; +import { + Coins, + Plus, + MoreHorizontal, + Search, + AlertTriangle, + TrendingUp, + Trash2, +} from 'lucide-react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; + +interface BudgetPolicy { + id: string; + name: string; + scope: string; + period: string; + limitAmount: number; + currentSpend?: number; + alertThresholdPercent: number; + status: string; + createdAt: string; +} + +interface BudgetAlert { + id: string; + policyId: string; + policyName?: string; + type: string; + message: string; + currentSpend: number; + limitAmount: number; + createdAt: string; + acknowledged: boolean; +} + +function formatCurrency(amount: number) { + return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount); +} + +function formatDate(iso: string) { + return new Date(iso).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }); +} + +async function apiFetch(path: string, opts?: RequestInit) { + const res = await fetch(`/api/ai-budgets/${path}`, { + headers: { 'Content-Type': 'application/json' }, + ...opts, + }); + return res.json(); +} + +export default function AIBudgetsPage() { + const [policies, setPolicies] = useState([]); + const [alerts, setAlerts] = useState([]); + const [loading, setLoading] = useState(true); + const [tab, setTab] = useState<'policies' | 'alerts'>('policies'); + const [showCreate, setShowCreate] = useState(false); + const [creating, setCreating] = useState(false); + + // Create form + const [newName, setNewName] = useState(''); + const [newScope, setNewScope] = useState('global'); + const [newPeriod, setNewPeriod] = useState('monthly'); + const [newLimit, setNewLimit] = useState('100'); + const [newThreshold, setNewThreshold] = useState('80'); + + const loadData = useCallback(async () => { + setLoading(true); + const [pData, aData] = await Promise.all([apiFetch('policies'), apiFetch('alerts')]); + setPolicies( + Array.isArray(pData?.policies) ? pData.policies : Array.isArray(pData) ? pData : [] + ); + setAlerts(Array.isArray(aData?.alerts) ? aData.alerts : Array.isArray(aData) ? aData : []); + setLoading(false); + }, []); + + useEffect(() => { + void loadData(); + }, [loadData]); + + async function handleCreate() { + setCreating(true); + await apiFetch('policies', { + method: 'POST', + body: JSON.stringify({ + name: newName, + scope: newScope, + period: newPeriod, + limitAmount: parseFloat(newLimit) || 100, + alertThresholdPercent: parseInt(newThreshold) || 80, + }), + }); + setCreating(false); + setShowCreate(false); + setNewName(''); + setNewScope('global'); + setNewPeriod('monthly'); + setNewLimit('100'); + setNewThreshold('80'); + loadData(); + } + + async function handleDelete(id: string) { + if (!confirm('Delete this budget policy?')) return; + await apiFetch(`policies/${id}`, { method: 'DELETE' }); + loadData(); + } + + const totalSpend = policies.reduce((sum, p) => sum + (p.currentSpend ?? 0), 0); + const totalBudget = policies.reduce((sum, p) => sum + p.limitAmount, 0); + const unacknowledgedAlerts = alerts.filter(a => !a.acknowledged).length; + + return ( +
+
+
+

AI Budgets & Cost Management

+

+ Monitor AI spend, set budget policies, and manage cost alerts +

+
+ +
+ +
+ + + Total Spend + + +
{formatCurrency(totalSpend)}
+
+
+ + + + Total Budget + + + +
{formatCurrency(totalBudget)}
+
+
+ + + Utilization + + +
+
+ {totalBudget > 0 ? Math.round((totalSpend / totalBudget) * 100) : 0}% +
+ +
+
+
+ + + + Active Alerts + + + +
+
{unacknowledgedAlerts}
+ {unacknowledgedAlerts > 0 && } +
+
+
+
+ +
+ + +
+ + {tab === 'policies' && ( + + + {loading ? ( +
Loading...
+ ) : policies.length === 0 ? ( +
+ + No budget policies yet. Create one to start tracking AI spend. +
+ ) : ( + + + + Policy + Scope + Period + Spend / Limit + Utilization + Status + + + + + {policies.map(p => { + const util = + p.limitAmount > 0 + ? Math.round(((p.currentSpend ?? 0) / p.limitAmount) * 100) + : 0; + return ( + + {p.name} + + + {p.scope} + + + {p.period} + + {formatCurrency(p.currentSpend ?? 0)} / {formatCurrency(p.limitAmount)} + + +
+
+
= 90 + ? 'bg-red-500' + : util >= 70 + ? 'bg-amber-500' + : 'bg-emerald-500' + }`} + style={{ width: `${Math.min(util, 100)}%` }} + /> +
+ {util}% +
+ + + + {p.status} + + + + + + + + + handleDelete(p.id)} + className="text-destructive" + > + Delete + + + + + + ); + })} + +
+ )} +
+
+ )} + + {tab === 'alerts' && ( + + + {alerts.length === 0 ? ( +
+ + No budget alerts. +
+ ) : ( + + + + Alert + Spend / Limit + Date + Status + + + + {alerts.map(a => ( + + +
{a.message}
+

+ {a.policyName || a.policyId} +

+
+ + {formatCurrency(a.currentSpend)} / {formatCurrency(a.limitAmount)} + + + {formatDate(a.createdAt)} + + + + {a.acknowledged ? 'Acknowledged' : 'Active'} + + +
+ ))} +
+
+ )} +
+
+ )} + + {/* Create Policy Dialog */} + + + + Create Budget Policy + + Set a spending limit and alert threshold for AI operations. + + +
+
+ + setNewName(e.target.value)} + /> +
+
+
+ + +
+
+ + +
+
+
+
+ + setNewLimit(e.target.value)} + /> +
+
+ + setNewThreshold(e.target.value)} + /> +
+
+
+ + + + +
+
+
+ ); +} diff --git a/dashboards/admin-web/src/app/(dashboard)/organizations/page.tsx b/dashboards/admin-web/src/app/(dashboard)/organizations/page.tsx new file mode 100644 index 00000000..c9c65b2d --- /dev/null +++ b/dashboards/admin-web/src/app/(dashboard)/organizations/page.tsx @@ -0,0 +1,409 @@ +'use client'; + +import { useState, useEffect, useCallback } from 'react'; +import { Building2, Plus, MoreHorizontal, Users, FolderOpen, Search, Trash2 } from 'lucide-react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from '@/components/ui/sheet'; + +interface Org { + id: string; + name: string; + slug: string; + status: string; + memberCount?: number; + workspaceCount?: number; + createdAt: string; +} + +interface Member { + id: string; + userId: string; + role: string; + email?: string; + name?: string; + createdAt: string; +} + +interface Workspace { + id: string; + name: string; + slug: string; + createdAt: string; +} + +function formatDate(iso: string) { + return new Date(iso).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }); +} + +async function apiFetch(path: string, opts?: RequestInit) { + const res = await fetch(`/api/orgs/${path}`, { + headers: { 'Content-Type': 'application/json' }, + ...opts, + }); + return res.json(); +} + +export default function OrganizationsPage() { + const [orgs, setOrgs] = useState([]); + const [total, setTotal] = useState(0); + const [loading, setLoading] = useState(true); + const [search, setSearch] = useState(''); + const [showCreate, setShowCreate] = useState(false); + const [newName, setNewName] = useState(''); + const [newSlug, setNewSlug] = useState(''); + const [creating, setCreating] = useState(false); + + // Detail drawer + const [selectedOrg, setSelectedOrg] = useState(null); + const [members, setMembers] = useState([]); + const [workspaces, setWorkspaces] = useState([]); + const [detailLoading, setDetailLoading] = useState(false); + + const loadOrgs = useCallback(async () => { + setLoading(true); + const qs = search ? `?search=${encodeURIComponent(search)}` : ''; + const data = await apiFetch(`list${qs}`); + if (data?.organizations) { + setOrgs(data.organizations); + setTotal(data.total ?? data.organizations.length); + } else if (Array.isArray(data)) { + setOrgs(data); + setTotal(data.length); + } + setLoading(false); + }, [search]); + + useEffect(() => { + void loadOrgs(); + }, [loadOrgs]); + + async function handleCreate() { + setCreating(true); + await apiFetch('create', { + method: 'POST', + body: JSON.stringify({ name: newName, slug: newSlug || undefined }), + }); + setCreating(false); + setShowCreate(false); + setNewName(''); + setNewSlug(''); + loadOrgs(); + } + + async function handleDelete(id: string) { + if (!confirm('Delete this organization? This cannot be undone.')) return; + await apiFetch(`${id}/delete`, { method: 'DELETE' }); + loadOrgs(); + if (selectedOrg?.id === id) setSelectedOrg(null); + } + + async function openDetail(org: Org) { + setSelectedOrg(org); + setDetailLoading(true); + const [membersData, wsData] = await Promise.all([ + apiFetch(`${org.id}/members`), + apiFetch(`${org.id}/workspaces`), + ]); + setMembers( + Array.isArray(membersData?.memberships) + ? membersData.memberships + : Array.isArray(membersData) + ? membersData + : [] + ); + setWorkspaces( + Array.isArray(wsData?.workspaces) ? wsData.workspaces : Array.isArray(wsData) ? wsData : [] + ); + setDetailLoading(false); + } + + const activeOrgs = orgs.filter(o => o.status === 'active'); + + return ( +
+
+
+

Organizations

+

+ Manage organizations, workspaces, and memberships +

+
+ +
+ +
+ + + + Total Organizations + + + +
{total}
+
+
+ + + Active + + +
{activeOrgs.length}
+
+
+ + + + Total Members + + + +
+ {orgs.reduce((sum, o) => sum + (o.memberCount ?? 0), 0)} +
+
+
+
+ +
+
+ + setSearch(e.target.value)} + className="pl-9" + /> +
+
+ + + + {loading ? ( +
Loading...
+ ) : orgs.length === 0 ? ( +
+ + No organizations yet. Create one to get started. +
+ ) : ( + + + + Organization + Slug + Members + Workspaces + Status + Created + + + + + {orgs.map(org => ( + openDetail(org)}> + {org.name} + + {org.slug} + + +
+ + {org.memberCount ?? 0} +
+
+ +
+ + {org.workspaceCount ?? 0} +
+
+ + + {org.status} + + + + {formatDate(org.createdAt)} + + e.stopPropagation()}> + + + + + + openDetail(org)}> + View Details + + handleDelete(org.id)} + className="text-destructive" + > + Delete + + + + +
+ ))} +
+
+ )} +
+
+ + {/* Create Dialog */} + + + + Create Organization + + Create a new organization with workspaces and members. + + +
+
+ + setNewName(e.target.value)} + /> +
+
+ + setNewSlug(e.target.value)} + /> +
+
+ + + + +
+
+ + {/* Detail Sheet */} + !open && setSelectedOrg(null)}> + + + {selectedOrg?.name} + + {selectedOrg?.id} + + + {detailLoading ? ( +
Loading...
+ ) : ( +
+
+

+ Members ({members.length}) +

+ {members.length === 0 ? ( +

No members yet.

+ ) : ( +
+ {members.map(m => ( +
+
+

{m.name || m.email || m.userId}

+

{m.email}

+
+ + {m.role} + +
+ ))} +
+ )} +
+ +
+

+ Workspaces ({workspaces.length}) +

+ {workspaces.length === 0 ? ( +

No workspaces yet.

+ ) : ( +
+ {workspaces.map(ws => ( +
+
+

{ws.name}

+ {ws.slug} +
+ + {formatDate(ws.createdAt)} + +
+ ))} +
+ )} +
+
+ )} +
+
+
+ ); +} diff --git a/dashboards/admin-web/src/app/(dashboard)/support/page.tsx b/dashboards/admin-web/src/app/(dashboard)/support/page.tsx new file mode 100644 index 00000000..fa38b734 --- /dev/null +++ b/dashboards/admin-web/src/app/(dashboard)/support/page.tsx @@ -0,0 +1,371 @@ +'use client'; + +import { useState, useEffect, useCallback } from 'react'; +import { + LifeBuoy, + Plus, + MoreHorizontal, + Search, + AlertTriangle, + CheckCircle2, + Clock, + XCircle, + ArrowUpRight, +} from 'lucide-react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; + +interface SupportCase { + id: string; + title: string; + description: string; + status: string; + priority: string; + requesterUserId: string; + assignedTo?: string; + createdAt: string; + updatedAt: string; +} + +const statusConfig: Record = { + open: { label: 'Open', color: 'bg-blue-50 text-blue-700', icon: Clock }, + in_progress: { label: 'In Progress', color: 'bg-amber-50 text-amber-700', icon: ArrowUpRight }, + waiting_on_customer: { label: 'Waiting', color: 'bg-purple-50 text-purple-700', icon: Clock }, + resolved: { label: 'Resolved', color: 'bg-emerald-50 text-emerald-700', icon: CheckCircle2 }, + closed: { label: 'Closed', color: 'bg-gray-50 text-gray-600', icon: XCircle }, +}; + +const priorityConfig: Record = { + low: { label: 'Low', color: 'bg-gray-50 text-gray-600' }, + medium: { label: 'Medium', color: 'bg-blue-50 text-blue-700' }, + high: { label: 'High', color: 'bg-amber-50 text-amber-700' }, + critical: { label: 'Critical', color: 'bg-red-50 text-red-700' }, +}; + +function formatDate(iso: string) { + return new Date(iso).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }); +} + +async function apiFetch(path: string, opts?: RequestInit) { + const res = await fetch(`/api/support/${path}`, { + headers: { 'Content-Type': 'application/json' }, + ...opts, + }); + return res.json(); +} + +export default function SupportCasesPage() { + const [cases, setCases] = useState([]); + const [total, setTotal] = useState(0); + const [loading, setLoading] = useState(true); + const [statusFilter, setStatusFilter] = useState('all'); + const [search, setSearch] = useState(''); + const [showCreate, setShowCreate] = useState(false); + const [creating, setCreating] = useState(false); + + // Create form + const [newTitle, setNewTitle] = useState(''); + const [newDescription, setNewDescription] = useState(''); + const [newPriority, setNewPriority] = useState('medium'); + + const loadCases = useCallback(async () => { + setLoading(true); + const params = new URLSearchParams(); + if (statusFilter !== 'all') params.set('status', statusFilter); + if (search) params.set('search', search); + const qs = params.toString() ? `?${params.toString()}` : ''; + const data = await apiFetch(`cases${qs}`); + if (data?.cases) { + setCases(data.cases); + setTotal(data.total ?? data.cases.length); + } else if (Array.isArray(data)) { + setCases(data); + setTotal(data.length); + } + setLoading(false); + }, [statusFilter, search]); + + useEffect(() => { + void loadCases(); + }, [loadCases]); + + async function handleCreate() { + setCreating(true); + await apiFetch('cases', { + method: 'POST', + body: JSON.stringify({ + title: newTitle, + description: newDescription, + priority: newPriority, + }), + }); + setCreating(false); + setShowCreate(false); + setNewTitle(''); + setNewDescription(''); + setNewPriority('medium'); + loadCases(); + } + + async function handleStatusChange(id: string, status: string) { + await apiFetch(`cases/${id}`, { + method: 'PATCH', + body: JSON.stringify({ status }), + }); + loadCases(); + } + + const openCount = cases.filter(c => c.status === 'open' || c.status === 'in_progress').length; + const criticalCount = cases.filter( + c => c.priority === 'critical' && c.status !== 'closed' && c.status !== 'resolved' + ).length; + + return ( +
+
+
+

Support Cases

+

+ Manage customer support cases, notes, and escalations +

+
+ +
+ +
+ + + Total Cases + + +
{total}
+
+
+ + + Open + + +
{openCount}
+
+
+ + + Critical + + +
+
{criticalCount}
+ {criticalCount > 0 && } +
+
+
+ + + Resolved + + +
+ {cases.filter(c => c.status === 'resolved' || c.status === 'closed').length} +
+
+
+
+ +
+
+ + setSearch(e.target.value)} + className="pl-9" + /> +
+ +
+ + + + {loading ? ( +
Loading...
+ ) : cases.length === 0 ? ( +
+ + No support cases found. +
+ ) : ( + + + + Title + Priority + Status + Assigned To + Created + + + + + {cases.map(c => { + const sCfg = statusConfig[c.status] || statusConfig.open; + const pCfg = priorityConfig[c.priority] || priorityConfig.medium; + const StatusIcon = sCfg.icon; + return ( + + +
{c.title}
+

+ {c.description} +

+
+ + {pCfg.label} + + + + + {sCfg.label} + + + + {c.assignedTo || '—'} + + + {formatDate(c.createdAt)} + + + + + + + + handleStatusChange(c.id, 'in_progress')} + > + Mark In Progress + + handleStatusChange(c.id, 'resolved')}> + Resolve + + handleStatusChange(c.id, 'closed')}> + Close + + + + +
+ ); + })} +
+
+ )} +
+
+ + {/* Create Dialog */} + + + + Create Support Case + Open a new support case for a customer issue. + +
+
+ + setNewTitle(e.target.value)} + /> +
+
+ +