diff --git a/app/admin/companies/[id]/page.tsx b/app/admin/companies/[id]/page.tsx new file mode 100644 index 000000000..491b03a9b --- /dev/null +++ b/app/admin/companies/[id]/page.tsx @@ -0,0 +1,626 @@ +"use client" + +import { useState, useEffect, useCallback } from "react" +import { useParams, useRouter } from "next/navigation" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" +import { + Building2, + Mail, + Globe, + MapPin, + Users, + Calendar, + CheckCircle, + XCircle, + Ban, + ArrowLeft, + ExternalLink, + FileText, + TrendingUp, +} from "lucide-react" +import { toast } from "sonner" +import { apiFetch } from "@/lib/api-fetch" +import Link from "next/link" +import type { Company, CompanyMember } from "@/types/company" + +interface Event { + id: string + title: string + slug: string + date: string + approval_status: string + registered: number +} + +export default function AdminCompanyDetailsPage() { + const params = useParams() + const router = useRouter() + const companyId = params.id as string + + const [company, setCompany] = useState(null) + const [members, setMembers] = useState([]) + const [events, setEvents] = useState([]) + const [loading, setLoading] = useState(true) + const [actionLoading, setActionLoading] = useState(false) + const [confirmAction, setConfirmAction] = useState<'verify' | 'reject' | 'suspend' | null>(null) + + const fetchCompanyDetails = useCallback(async () => { + try { + setLoading(true) + + // Fetch company details + const companyResponse = await apiFetch(`/api/admin/companies/${companyId}`) + if (!companyResponse.ok) { + throw new Error("Failed to fetch company details") + } + const companyData = await companyResponse.json() + console.log('Company API Response:', companyData) // Debug log + setCompany(companyData.company) + + // Fetch company members + const membersResponse = await apiFetch(`/api/companies/${companyData.company.slug}/members`) + if (membersResponse.ok) { + const membersData = await membersResponse.json() + setMembers(membersData.members || []) + } + + // Fetch company events + const eventsResponse = await apiFetch(`/api/companies/${companyData.company.slug}/events`) + if (eventsResponse.ok) { + const eventsData = await eventsResponse.json() + setEvents(eventsData.events || []) + } + } catch (error) { + toast.error("Failed to fetch company details") + console.error("Fetch error:", error) + } finally { + setLoading(false) + } + }, [companyId]) + + useEffect(() => { + if (companyId) { + fetchCompanyDetails() + } + }, [companyId, fetchCompanyDetails]) + + const handleVerify = async () => { + try { + setActionLoading(true) + const response = await apiFetch(`/api/admin/companies/${companyId}/verify`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ notes: "Approved by admin" }), + }) + + if (!response.ok) { + throw new Error("Failed to verify company") + } + + toast.success("Company has been verified") + fetchCompanyDetails() + } catch (error) { + toast.error("Failed to verify company") + console.error("Verify error:", error) + } finally { + setActionLoading(false) + setConfirmAction(null) + } + } + + const handleReject = async () => { + try { + setActionLoading(true) + const response = await apiFetch(`/api/admin/companies/${companyId}/reject`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ reason: "Verification requirements not met" }), + }) + + if (!response.ok) { + throw new Error("Failed to reject company") + } + + toast.success("Company verification has been rejected") + fetchCompanyDetails() + } catch (error) { + toast.error("Failed to reject company") + console.error("Reject error:", error) + } finally { + setActionLoading(false) + setConfirmAction(null) + } + } + + const handleSuspend = async () => { + try { + setActionLoading(true) + const response = await apiFetch(`/api/admin/companies/${companyId}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ status: "suspended" }), + }) + + if (!response.ok) { + throw new Error("Failed to suspend company") + } + + toast.success("Company has been suspended") + fetchCompanyDetails() + } catch (error) { + toast.error("Failed to suspend company") + console.error("Suspend error:", error) + } finally { + setActionLoading(false) + setConfirmAction(null) + } + } + + const getVerificationBadge = (status: string) => { + switch (status) { + case "verified": + return ( + + + Verified + + ) + case "pending": + return ( + + Pending Review + + ) + case "rejected": + return ( + + + Rejected + + ) + default: + return Unknown + } + } + + const getStatusBadge = (status: string) => { + switch (status) { + case "active": + return Active + case "suspended": + return Suspended + case "deleted": + return Deleted + default: + return Unknown + } + } + + const getApprovalBadge = (status: string) => { + switch (status) { + case "approved": + return Approved + case "pending": + return Pending + case "rejected": + return Rejected + default: + return Unknown + } + } + + const getRoleBadge = (role: string) => { + switch (role) { + case "owner": + return Owner + case "admin": + return Admin + case "editor": + return Editor + case "member": + return Member + default: + return Unknown + } + } + + if (loading) { + return ( +
+
+
+ ) + } + + if (!company) { + return ( +
+
+

Company Not Found

+

The company you're looking for doesn't exist.

+ +
+
+ ) + } + + return ( +
+ {/* Header */} +
+
+ +
+

{company.name}

+

{company.email}

+
+
+
+ {getVerificationBadge(company.verification_status)} + {getStatusBadge(company.status)} +
+
+ + {/* Quick Actions */} + {company.verification_status === "pending" && ( + + + + + Pending Verification + + This company is awaiting verification approval + + +
+ + + +
+
+
+ )} + + {company.status === "active" && company.verification_status === "verified" && ( +
+ +
+ )} + + {/* Stats Cards */} +
+ + + Total Events + + + +
{company.total_events || 0}
+
+
+ + + + Team Members + + + +
{members.length}
+
+
+ + + + Total Participants + + + +
{company.total_participants || 0}
+
+
+ + + + Subscription + + + +
{company.subscription_tier}
+
+
+
+ + {/* Tabs */} + + + Overview + Events ({events.length}) + Team ({members.length}) + + + + + + Company Information + + +
+
+ +

{company.name}

+
+
+ +

{company.legal_name || "—"}

+
+
+ +
+ +

{company.email}

+
+
+
+ +
+ + {company.website ? ( + + {company.website} + + + ) : ( +

+ )} +
+
+
+ +

{company.industry || "—"}

+
+
+ +

{company.company_size || "—"}

+
+
+ +

{new Date(company.created_at).toLocaleDateString()}

+
+ {company.verified_at && ( +
+ +

{new Date(company.verified_at).toLocaleDateString()}

+
+ )} +
+ + {company.description && ( +
+ +

{company.description}

+
+ )} + + {company.address && ( +
+ +
+ +

+ {[ + company.address.street, + company.address.city, + company.address.state, + company.address.country, + company.address.zip, + ] + .filter(Boolean) + .join(", ")} +

+
+
+ )} +
+
+
+ + + + + Company Events + All events created by this company + + + {events.length === 0 ? ( +
+ +

No events yet

+

This company hasn't created any events

+
+ ) : ( + + + + Event Title + Date + Status + Registrations + Actions + + + + {events.map((event) => ( + + {event.title} + {new Date(event.date).toLocaleDateString()} + {getApprovalBadge(event.approval_status)} + {event.registered || 0} + + + + + ))} + +
+ )} +
+
+
+ + + + + Team Members + All members associated with this company + + + {members.length === 0 ? ( +
+ +

No team members

+

This company has no team members

+
+ ) : ( + + + + Member + Role + Status + Joined + + + + {members.map((member) => ( + + +
+

+ {member.user?.first_name || member.user?.email || "Unknown"} +

+

{member.user?.email}

+
+
+ {getRoleBadge(member.role)} + + + {member.status} + + + {new Date(member.joined_at).toLocaleDateString()} +
+ ))} +
+
+ )} +
+
+
+
+ + {/* Confirmation Dialog */} + setConfirmAction(null)}> + + + + {confirmAction === 'verify' && 'Verify Company'} + {confirmAction === 'reject' && 'Reject Verification'} + {confirmAction === 'suspend' && 'Suspend Company'} + + + {confirmAction === 'verify' && + `Are you sure you want to verify ${company.name}? This will allow them to create and manage events.`} + {confirmAction === 'reject' && + `Are you sure you want to reject ${company.name}'s verification? They will be notified of this decision.`} + {confirmAction === 'suspend' && + `Are you sure you want to suspend ${company.name}? This will prevent them from accessing their dashboard and managing events.`} + + + + Cancel + { + if (confirmAction === 'verify') handleVerify() + else if (confirmAction === 'reject') handleReject() + else if (confirmAction === 'suspend') handleSuspend() + }} + className={ + confirmAction === 'verify' + ? 'bg-green-600 hover:bg-green-700' + : 'bg-red-600 hover:bg-red-700' + } + > + {confirmAction === 'verify' && 'Verify'} + {confirmAction === 'reject' && 'Reject'} + {confirmAction === 'suspend' && 'Suspend'} + + + + +
+ ) +} diff --git a/app/admin/companies/[id]/verify/page.tsx b/app/admin/companies/[id]/verify/page.tsx new file mode 100644 index 000000000..7f51fe556 --- /dev/null +++ b/app/admin/companies/[id]/verify/page.tsx @@ -0,0 +1,650 @@ +"use client" + +import { useState, useEffect, useCallback } from "react" +import { useParams, useRouter } from "next/navigation" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Textarea } from "@/components/ui/textarea" +import { Label } from "@/components/ui/label" +import { Separator } from "@/components/ui/separator" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" +import { + Building2, + Mail, + Globe, + MapPin, + ArrowLeft, + CheckCircle, + XCircle, + FileText, + Download, + ExternalLink, + Calendar, + User, + AlertCircle, +} from "lucide-react" +import { toast } from "sonner" +import { apiFetch } from "@/lib/api-fetch" +import type { Company } from "@/types/company" + +interface CompanyWithRelations extends Company { + verified_by_profile?: { + id: string + email: string + first_name?: string + last_name?: string + } + settings?: { + rejected_at?: string + rejection_reason?: string + rejection_notes?: string + rejected_by?: string + [key: string]: unknown + } +} + +export default function CompanyVerificationPage() { + const params = useParams() + const router = useRouter() + const companyId = params.id as string + + const [company, setCompany] = useState(null) + const [loading, setLoading] = useState(true) + const [actionLoading, setActionLoading] = useState(false) + const [verificationNotes, setVerificationNotes] = useState("") + const [rejectionReason, setRejectionReason] = useState("") + const [confirmAction, setConfirmAction] = useState<'verify' | 'reject' | null>(null) + const [documentUrls, setDocumentUrls] = useState([]) + + const fetchCompanyDetails = useCallback(async () => { + try { + setLoading(true) + + const response = await apiFetch(`/api/admin/companies/${companyId}`) + if (!response.ok) { + throw new Error("Failed to fetch company details") + } + const data = await response.json() + setCompany(data.data.company) + + // Get signed URLs for verification documents + if (data.data.company.verification_documents && data.data.company.verification_documents.length > 0) { + const urls = await Promise.all( + data.data.company.verification_documents.map(async (path: string) => { + try { + const urlResponse = await apiFetch(`/api/companies/${data.data.company.slug}/documents?path=${encodeURIComponent(path)}`) + if (urlResponse.ok) { + const urlData = await urlResponse.json() + return urlData.url + } + return null + } catch (error) { + console.error("Error fetching document URL:", error) + return null + } + }) + ) + setDocumentUrls(urls.filter(Boolean)) + } + } catch (error) { + toast.error("Failed to fetch company details") + console.error("Fetch error:", error) + } finally { + setLoading(false) + } + }, [companyId]) + + useEffect(() => { + if (companyId) { + fetchCompanyDetails() + } + }, [companyId, fetchCompanyDetails]) + + const handleVerify = async () => { + try { + setActionLoading(true) + const response = await apiFetch(`/api/admin/companies/${companyId}/verify`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ notes: verificationNotes }), + }) + + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.error || "Failed to verify company") + } + + toast.success("Company has been verified successfully") + router.push(`/admin/companies/${companyId}`) + } catch (error) { + toast.error(error instanceof Error ? error.message : "Failed to verify company") + console.error("Verify error:", error) + } finally { + setActionLoading(false) + setConfirmAction(null) + } + } + + const handleReject = async () => { + if (!rejectionReason.trim()) { + toast.error("Please provide a rejection reason") + return + } + + try { + setActionLoading(true) + const response = await apiFetch(`/api/admin/companies/${companyId}/reject`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ reason: rejectionReason }), + }) + + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.error || "Failed to reject company") + } + + toast.success("Company verification has been rejected") + router.push(`/admin/companies/${companyId}`) + } catch (error) { + toast.error(error instanceof Error ? error.message : "Failed to reject company") + console.error("Reject error:", error) + } finally { + setActionLoading(false) + setConfirmAction(null) + } + } + + const downloadDocument = async (url: string, index: number) => { + try { + const response = await fetch(url) + const blob = await response.blob() + const downloadUrl = window.URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = downloadUrl + link.download = `verification-document-${index + 1}.pdf` + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + window.URL.revokeObjectURL(downloadUrl) + toast.success("Document downloaded") + } catch (error) { + toast.error("Failed to download document") + console.error("Download error:", error) + } + } + + if (loading) { + return ( +
+
+
+ ) + } + + if (!company) { + return ( +
+
+

Company Not Found

+

The company you're looking for doesn't exist.

+ +
+
+ ) + } + + const isAlreadyProcessed = company.verification_status !== 'pending' + + return ( +
+ {/* Header */} +
+
+ +
+

Company Verification

+

{company.name}

+
+
+ + {company.verification_status === "verified" && } + {company.verification_status === "rejected" && } + {company.verification_status.charAt(0).toUpperCase() + company.verification_status.slice(1)} + +
+ + {/* Alert for already processed */} + {isAlreadyProcessed && ( + + +
+ +
+

Already Processed

+

+ This company has already been {company.verification_status}. + {company.verified_at && ` Processed on ${new Date(company.verified_at).toLocaleDateString()}.`} +

+
+
+
+
+ )} + +
+ {/* Left Column - Company Information */} +
+ {/* Company Details */} + + + + + Company Information + + Review the company's registration details + + +
+ +

{company.name}

+
+ + {company.legal_name && ( +
+ +

{company.legal_name}

+
+ )} + + + +
+ +
+ +

{company.email}

+
+
+ + {company.phone && ( +
+ +

{company.phone}

+
+ )} + +
+ +
+ + {company.website ? ( + + {company.website} + + + ) : ( +

+ )} +
+
+ + + +
+
+ +

{company.industry || "—"}

+
+
+ +

{company.company_size || "—"}

+
+
+ + {company.description && ( + <> + +
+ +

{company.description}

+
+ + )} + + {company.address && ( + <> + +
+ +
+ +

+ {[ + company.address.street, + company.address.city, + company.address.state, + company.address.country, + company.address.zip, + ] + .filter(Boolean) + .join(", ")} +

+
+
+ + )} + + + +
+
+ +
+ + {new Date(company.created_at).toLocaleDateString()} +
+
+ {company.verified_at && ( +
+ +
+ + {new Date(company.verified_at).toLocaleDateString()} +
+
+ )} +
+
+
+ + {/* Verification Documents */} + + + + + Verification Documents + + Review uploaded verification documents + + + {!company.verification_documents || company.verification_documents.length === 0 ? ( +
+ +

No Documents Uploaded

+

+ This company hasn't uploaded any verification documents +

+
+ ) : ( +
+ {company.verification_documents.map((doc, index) => ( +
+
+
+ +
+
+

Document {index + 1}

+

+ {doc.split('/').pop()?.substring(0, 30)}... +

+
+
+
+ {documentUrls[index] && ( + <> + + + + )} +
+
+ ))} +
+ )} +
+
+
+ + {/* Right Column - Verification Actions */} +
+ {/* Verification History */} + + + + + Verification History + + Track of verification status changes + + +
+
+
+ +
+
+

Company Registered

+

+ {new Date(company.created_at).toLocaleString()} +

+
+
+ + {company.verified_at && ( +
+
+ +
+
+

Company Verified

+

+ {new Date(company.verified_at).toLocaleString()} +

+ {company.verified_by_profile && ( +

+ By: {company.verified_by_profile.email} +

+ )} +
+
+ )} + + {company.verification_status === 'rejected' && company.settings?.rejected_at && ( +
+
+ +
+
+

Verification Rejected

+

+ {new Date(company.settings.rejected_at).toLocaleString()} +

+ {company.settings.rejection_reason && ( +

+ Reason: {company.settings.rejection_reason} +

+ )} +
+
+ )} +
+
+
+ + {/* Verification Form */} + {!isAlreadyProcessed && ( + + + Verification Decision + Approve or reject this company's verification + + +
+ +