diff --git a/admin/src/app/dashboard/EnterprisePage.tsx b/admin/src/app/dashboard/EnterprisePage.tsx new file mode 100644 index 00000000..daca9c10 --- /dev/null +++ b/admin/src/app/dashboard/EnterprisePage.tsx @@ -0,0 +1,183 @@ +// Sales pipeline for inquiries submitted from the marketing site. +// Status flow: pending → contacted → converted | declined. + +import { useState } from "react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; +import { PageHeader } from "@/components/layout/PageHeader"; +import { Badge } from "@/components/ui/badge"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + listEnterpriseInquiries, + updateEnterpriseInquiry, +} from "@/lib/api/client/admin/enterprise"; +import type { + EnterpriseInquiry, + EnterpriseInquiryStatus, +} from "@/lib/api/models/admin"; + +const STATUS_TONE: Record = { + pending: "border-amber-300 text-amber-700 bg-amber-50", + contacted: "border-blue-300 text-blue-700 bg-blue-50", + converted: "border-emerald-300 text-emerald-700 bg-emerald-50", + declined: "border-zinc-300 text-zinc-600 bg-zinc-50", +}; + +const STATUSES: EnterpriseInquiryStatus[] = [ + "pending", + "contacted", + "converted", + "declined", +]; + +type StatusFilter = EnterpriseInquiryStatus | "all"; + +export default function EnterprisePage() { + const [status, setStatus] = useState("pending"); + const { data, isLoading, error } = useQuery({ + queryKey: ["admin", "enterprise", "inquiries", status], + queryFn: () => listEnterpriseInquiries(status === "all" ? undefined : status), + staleTime: 30_000, + }); + + const rows = data?.data ?? []; + + return ( +
+ + + + + {isLoading && } + {error && ( +
+ Failed to load inquiries. +
+ )} + + {!isLoading && !error && rows.length === 0 && ( +
+ No inquiries in this status. +
+ )} + + {rows.length > 0 && ( +
+ + + + + + + + + + + + + + {rows.map((i) => ( + + ))} + +
CompanyContactVolumeTeamNotesStatusReceived
+
+ )} +
+ ); +} + +function InquiryRow({ inquiry }: { inquiry: EnterpriseInquiry }) { + const qc = useQueryClient(); + const mutation = useMutation({ + mutationFn: (next: EnterpriseInquiryStatus) => + updateEnterpriseInquiry(inquiry.id, { status: next }), + onSuccess: () => { + toast.success("Inquiry updated"); + qc.invalidateQueries({ queryKey: ["admin", "enterprise"] }); + }, + onError: (err: Error) => toast.error(err.message || "Update failed"), + }); + + return ( + + +
{inquiry.company_name}
+ + +
{inquiry.contact_name}
+
{inquiry.contact_email}
+ + + {inquiry.estimated_volume != null + ? inquiry.estimated_volume.toLocaleString() + : "—"} + + + {inquiry.team_size != null ? inquiry.team_size : "—"} + + + {inquiry.notes || } + + + + + + {new Date(inquiry.created_at).toLocaleDateString()} + + + ); +} + +function StatusToggle({ + value, + onChange, +}: { + value: StatusFilter; + onChange: (v: StatusFilter) => void; +}) { + const options: { value: StatusFilter; label: string }[] = [ + { value: "pending", label: "Pending" }, + { value: "contacted", label: "Contacted" }, + { value: "converted", label: "Converted" }, + { value: "declined", label: "Declined" }, + { value: "all", label: "All" }, + ]; + return ( +
+ {options.map((opt) => ( + + ))} +
+ ); +} + diff --git a/admin/src/components/layout/Sidebar.tsx b/admin/src/components/layout/Sidebar.tsx index 4a616acd..14084d59 100644 --- a/admin/src/components/layout/Sidebar.tsx +++ b/admin/src/components/layout/Sidebar.tsx @@ -7,6 +7,7 @@ import { NavLink } from "react-router-dom"; import { Activity, BarChart3, + Briefcase, Building2, Cloud, Cog, @@ -66,6 +67,7 @@ const GROUPS: NavGroup[] = [ { to: "/users", label: "Users", icon: Users }, { to: "/organizations", label: "Organizations", icon: Building2 }, { to: "/plans", label: "Plans & Billing", icon: CreditCard }, + { to: "/enterprise", label: "Enterprise", icon: Briefcase }, ], }, { diff --git a/admin/src/lib/api/client/admin/enterprise.ts b/admin/src/lib/api/client/admin/enterprise.ts new file mode 100644 index 00000000..4b66fe81 --- /dev/null +++ b/admin/src/lib/api/client/admin/enterprise.ts @@ -0,0 +1,41 @@ +// /admin/enterprise/inquiries — sales-pipeline triage. + +import { Request } from "@/lib/api/client"; +import type { + EnterpriseInquiry, + UpdateEnterpriseInquiryRequest, +} from "@/lib/api/models/admin"; + +export function listEnterpriseInquiries( + status?: string, + limit = 50, +): Promise<{ data: EnterpriseInquiry[] }> { + const usp = new URLSearchParams(); + if (status) usp.set("status", status); + usp.set("limit", String(limit)); + return Request({ + method: "GET", + url: `/admin/enterprise/inquiries?${usp.toString()}`, + authorization: true, + }); +} + +export function getEnterpriseInquiry(id: string): Promise { + return Request({ + method: "GET", + url: `/admin/enterprise/inquiries/${id}`, + authorization: true, + }); +} + +export function updateEnterpriseInquiry( + id: string, + body: UpdateEnterpriseInquiryRequest, +): Promise { + return Request({ + method: "PATCH", + url: `/admin/enterprise/inquiries/${id}`, + authorization: true, + data: body, + }); +} diff --git a/admin/src/lib/api/models/admin.ts b/admin/src/lib/api/models/admin.ts index 535dbf76..56f8a3d1 100644 --- a/admin/src/lib/api/models/admin.ts +++ b/admin/src/lib/api/models/admin.ts @@ -323,6 +323,35 @@ export interface ProvisioningJobCreate { }; } +// /admin/enterprise/inquiries — sales pipeline for "talk to us" +// requests submitted from the marketing site. + +export type EnterpriseInquiryStatus = + | "pending" + | "contacted" + | "converted" + | "declined"; + +export interface EnterpriseInquiry { + id: string; + company_name: string; + contact_name: string; + contact_email: string; + estimated_volume?: number | null; + team_size?: number | null; + notes: string; + status: EnterpriseInquiryStatus; + created_at: string; + processed_at?: string | null; + processed_by?: string | null; +} + +export interface UpdateEnterpriseInquiryRequest { + status?: EnterpriseInquiryStatus; + assigned_to?: string; + notes?: string; +} + // /admin/campaigns/* — platform-wide campaign admin (force-stop runaway // campaigns, inspect engagement counters per campaign). diff --git a/admin/src/main.tsx b/admin/src/main.tsx index 36ecd5e0..964e5adb 100644 --- a/admin/src/main.tsx +++ b/admin/src/main.tsx @@ -39,6 +39,7 @@ import UsersPage from "@/app/dashboard/UsersPage"; import UserDetailPage from "@/app/dashboard/UserDetailPage"; import WarmupPage from "@/app/dashboard/WarmupPage"; import CampaignsPage from "@/app/dashboard/CampaignsPage"; +import EnterprisePage from "@/app/dashboard/EnterprisePage"; import { AnalyticsPage, MailboxesPage, @@ -91,6 +92,7 @@ const router = createBrowserRouter([ { path: "plans", element: }, { path: "warmup", element: }, { path: "campaigns", element: }, + { path: "enterprise", element: }, { path: "analytics", element: }, { path: "audit", element: }, {