From c5d1589712c74ca7b7cf4ab723f026b748fc9cb5 Mon Sep 17 00:00:00 2001 From: Akshay Date: Wed, 12 Nov 2025 12:56:07 +0530 Subject: [PATCH] feat(admin): Add comprehensive company details page for admin review - Implement detailed admin view for company profiles - Create dynamic tabs for company information, members, and events - Add verification, rejection, and suspension actions for companies - Integrate API endpoints for fetching and managing company details - Implement responsive design with detailed information cards - Include navigation and action buttons for administrative workflow - Handle loading states and error scenarios gracefully --- app/admin/companies/[id]/page.tsx | 626 ++++++++++++ app/admin/companies/[id]/verify/page.tsx | 650 ++++++++++++ app/admin/companies/page.tsx | 552 ++++++++++ app/admin/layout.tsx | 11 + app/admin/moderation/[id]/page.tsx | 208 ++++ app/admin/moderation/page.tsx | 137 +++ app/api/admin/companies/[id]/reject/route.ts | 144 +++ app/api/admin/companies/[id]/route.ts | 272 +++++ app/api/admin/companies/[id]/verify/route.ts | 130 +++ app/api/admin/companies/route.ts | 109 ++ .../moderation/events/[id]/approve/route.ts | 188 ++++ .../moderation/events/[id]/reject/route.ts | 200 ++++ .../events/[id]/request-changes/route.ts | 204 ++++ app/api/admin/moderation/events/[id]/route.ts | 81 ++ app/api/admin/moderation/events/route.ts | 54 + app/api/analytics/aggregate/route.ts | 146 +++ .../[slug]/analytics/export/route.ts | 118 +++ app/api/companies/[slug]/analytics/route.ts | 162 +++ app/api/companies/[slug]/assets/route.ts | 283 ++++++ app/api/companies/[slug]/documents/route.ts | 353 +++++++ app/api/companies/[slug]/events/route.ts | 164 +++ app/api/companies/[slug]/hackathons/route.ts | 115 +++ .../[slug]/members/[userId]/route.ts | 253 +++++ .../companies/[slug]/members/accept/route.ts | 86 ++ .../companies/[slug]/members/invite/route.ts | 150 +++ app/api/companies/[slug]/members/route.ts | 106 ++ app/api/companies/[slug]/route.ts | 303 ++++++ .../[slug]/subscription/cancel/route.ts | 61 ++ .../[slug]/subscription/upgrade/route.ts | 73 ++ .../[slug]/subscription/usage/route.ts | 57 ++ app/api/companies/me/route.ts | 43 + app/api/companies/register/route.ts | 177 ++++ app/api/companies/route.ts | 68 ++ app/api/events/[slug]/route.ts | 167 +++- app/api/events/[slug]/submit/route.ts | 96 ++ app/api/events/[slug]/track-click/route.ts | 69 ++ app/api/events/[slug]/track-view/route.ts | 69 ++ app/api/events/route.ts | 132 ++- app/api/hackathons/{[slug] => [id]}/route.ts | 36 +- app/api/hackathons/[id]/track-click/route.ts | 69 ++ app/api/hackathons/[id]/track-view/route.ts | 69 ++ app/api/hackathons/route.ts | 3 + app/api/notifications/[id]/route.ts | 91 ++ app/api/notifications/mark-all-read/route.ts | 41 + app/api/notifications/route.ts | 55 + app/companies/[slug]/events/page.tsx | 472 +++++++++ app/companies/[slug]/hackathons/page.tsx | 416 ++++++++ app/companies/[slug]/page.tsx | 165 +++ app/companies/faq/page.tsx | 61 ++ app/companies/page.tsx | 389 ++++++++ app/companies/register/page.tsx | 291 ++++++ .../company/[slug]/accept-invitation/page.tsx | 249 +++++ .../company/[slug]/analytics/page.tsx | 396 ++++++++ app/dashboard/company/[slug]/page.tsx | 174 ++++ .../company/[slug]/settings/page.tsx | 941 ++++++++++++++++++ .../company/[slug]/subscription/page.tsx | 69 ++ app/dashboard/company/[slug]/team/page.tsx | 68 ++ .../company/events/[slug]/edit/page.tsx | 167 ++++ app/dashboard/company/events/create/page.tsx | 53 + app/dashboard/company/events/page.tsx | 290 ++++++ app/dashboard/company/layout.tsx | 202 ++++ app/dashboard/company/page.tsx | 174 ++++ app/docs/[slug]/page.tsx | 140 +++ app/events/[slug]/page.tsx | 111 ++- app/events/page.tsx | 159 ++- app/hackathons/[id]/page.tsx | 29 +- app/hackathons/page.tsx | 183 +++- app/help/companies/page.tsx | 273 +++++ app/test-notifications/page.tsx | 176 ++++ components/companies/CompanyBadge.tsx | 79 ++ components/companies/CompanyCard.tsx | 96 ++ components/companies/CompanyProfile.tsx | 284 ++++++ .../companies/CompanyRegistrationForm.tsx | 693 +++++++++++++ components/companies/README.md | 100 ++ components/companies/VerificationBadge.tsx | 89 ++ components/companies/index.ts | 4 + components/dashboard/AnalyticsCharts.tsx | 513 ++++++++++ components/dashboard/CompanyDashboard.tsx | 575 +++++++++++ components/dashboard/CompanyHeader.tsx | 88 ++ components/dashboard/CompanySidebar.tsx | 458 +++++++++ components/dashboard/EventForm.tsx | 422 ++++++++ components/dashboard/TeamManagement.tsx | 599 +++++++++++ components/help/CompanyFAQ.tsx | 326 ++++++ components/help/HelpTooltip.tsx | 168 ++++ components/moderation/EventReview.tsx | 503 ++++++++++ components/moderation/ModerationQueue.tsx | 554 +++++++++++ components/moderation/README.md | 84 ++ .../notifications/NotificationCenter.tsx | 154 +++ components/notifications/NotificationItem.tsx | 126 +++ .../notifications/NotificationPreferences.tsx | 208 ++++ components/notifications/README.md | 210 ++++ components/notifications/index.ts | 4 + .../notifications/notification-utils.tsx | 56 ++ components/subscription/README.md | 285 ++++++ .../SubscriptionExpiryWarning.tsx | 111 +++ .../subscription/SubscriptionManagement.tsx | 450 +++++++++ components/subscription/UpgradePrompt.tsx | 230 +++++ components/subscription/index.ts | 3 + components/ui/breadcrumb.tsx | 110 ++ components/ui/calendar.tsx | 65 ++ components/ui/popover.tsx | 31 + components/ui/scroll-area.tsx | 48 + contexts/CompanyContext.tsx | 150 +++ docs/README.md | 262 +++++ hooks/useAnalyticsTracking.ts | 69 ++ hooks/useCompany.ts | 207 ++++ hooks/useCompanyAnalytics.ts | 236 +++++ hooks/useCompanyEvents.ts | 355 +++++++ hooks/useCompanyMembers.ts | 217 ++++ hooks/useEvents.ts | 6 + hooks/useHackathons.ts | 8 +- hooks/useModerationQueue.ts | 286 ++++++ hooks/useNotifications.ts | 216 ++++ hooks/useSubscription.ts | 107 ++ lib/email/company-emails.ts | 269 +++++ lib/email/templates/README.md | 102 ++ .../company-registration-confirmation.tsx | 118 +++ .../company-verification-approved.tsx | 140 +++ .../company-verification-rejected.tsx | 99 ++ lib/email/templates/event-approved.tsx | 148 +++ lib/email/templates/event-rejected.tsx | 110 ++ lib/email/templates/event-submitted.tsx | 127 +++ lib/email/templates/index.ts | 10 + .../new-registration-notification.tsx | 180 ++++ lib/email/templates/team-invitation.tsx | 146 +++ lib/services/analytics-service.ts | 294 ++++++ lib/services/authorization-service.ts | 594 +++++++++++ lib/services/company-member-service.ts | 741 ++++++++++++++ lib/services/company-service.ts | 670 +++++++++++++ lib/services/events.ts | 536 +++++++++- lib/services/hackathons.ts | 29 +- lib/services/moderation-service.ts | 882 ++++++++++++++++ lib/services/notification-service.ts | 275 +++++ lib/services/subscription-service.ts | 417 ++++++++ lib/storage/README.md | 313 ++++++ lib/storage/company-assets.ts | 345 +++++++ lib/storage/company-documents.ts | 373 +++++++ lib/storage/index.ts | 30 + package-lock.json | 543 +++++++--- package.json | 5 + scripts/check-admin-status.js | 116 +++ scripts/migrate-codeunia-company.js | 489 +++++++++ scripts/setup-test-users.js | 101 ++ scripts/test-migration.js | 202 ++++ types/company.ts | 203 ++++ types/events.ts | 17 + types/hackathons.ts | 17 + types/notifications.ts | 56 ++ vercel.json | 6 + 149 files changed, 31083 insertions(+), 294 deletions(-) create mode 100644 app/admin/companies/[id]/page.tsx create mode 100644 app/admin/companies/[id]/verify/page.tsx create mode 100644 app/admin/companies/page.tsx create mode 100644 app/admin/moderation/[id]/page.tsx create mode 100644 app/admin/moderation/page.tsx create mode 100644 app/api/admin/companies/[id]/reject/route.ts create mode 100644 app/api/admin/companies/[id]/route.ts create mode 100644 app/api/admin/companies/[id]/verify/route.ts create mode 100644 app/api/admin/companies/route.ts create mode 100644 app/api/admin/moderation/events/[id]/approve/route.ts create mode 100644 app/api/admin/moderation/events/[id]/reject/route.ts create mode 100644 app/api/admin/moderation/events/[id]/request-changes/route.ts create mode 100644 app/api/admin/moderation/events/[id]/route.ts create mode 100644 app/api/admin/moderation/events/route.ts create mode 100644 app/api/analytics/aggregate/route.ts create mode 100644 app/api/companies/[slug]/analytics/export/route.ts create mode 100644 app/api/companies/[slug]/analytics/route.ts create mode 100644 app/api/companies/[slug]/assets/route.ts create mode 100644 app/api/companies/[slug]/documents/route.ts create mode 100644 app/api/companies/[slug]/events/route.ts create mode 100644 app/api/companies/[slug]/hackathons/route.ts create mode 100644 app/api/companies/[slug]/members/[userId]/route.ts create mode 100644 app/api/companies/[slug]/members/accept/route.ts create mode 100644 app/api/companies/[slug]/members/invite/route.ts create mode 100644 app/api/companies/[slug]/members/route.ts create mode 100644 app/api/companies/[slug]/route.ts create mode 100644 app/api/companies/[slug]/subscription/cancel/route.ts create mode 100644 app/api/companies/[slug]/subscription/upgrade/route.ts create mode 100644 app/api/companies/[slug]/subscription/usage/route.ts create mode 100644 app/api/companies/me/route.ts create mode 100644 app/api/companies/register/route.ts create mode 100644 app/api/companies/route.ts create mode 100644 app/api/events/[slug]/submit/route.ts create mode 100644 app/api/events/[slug]/track-click/route.ts create mode 100644 app/api/events/[slug]/track-view/route.ts rename app/api/hackathons/{[slug] => [id]}/route.ts (57%) create mode 100644 app/api/hackathons/[id]/track-click/route.ts create mode 100644 app/api/hackathons/[id]/track-view/route.ts create mode 100644 app/api/notifications/[id]/route.ts create mode 100644 app/api/notifications/mark-all-read/route.ts create mode 100644 app/api/notifications/route.ts create mode 100644 app/companies/[slug]/events/page.tsx create mode 100644 app/companies/[slug]/hackathons/page.tsx create mode 100644 app/companies/[slug]/page.tsx create mode 100644 app/companies/faq/page.tsx create mode 100644 app/companies/page.tsx create mode 100644 app/companies/register/page.tsx create mode 100644 app/dashboard/company/[slug]/accept-invitation/page.tsx create mode 100644 app/dashboard/company/[slug]/analytics/page.tsx create mode 100644 app/dashboard/company/[slug]/page.tsx create mode 100644 app/dashboard/company/[slug]/settings/page.tsx create mode 100644 app/dashboard/company/[slug]/subscription/page.tsx create mode 100644 app/dashboard/company/[slug]/team/page.tsx create mode 100644 app/dashboard/company/events/[slug]/edit/page.tsx create mode 100644 app/dashboard/company/events/create/page.tsx create mode 100644 app/dashboard/company/events/page.tsx create mode 100644 app/dashboard/company/layout.tsx create mode 100644 app/dashboard/company/page.tsx create mode 100644 app/docs/[slug]/page.tsx create mode 100644 app/help/companies/page.tsx create mode 100644 app/test-notifications/page.tsx create mode 100644 components/companies/CompanyBadge.tsx create mode 100644 components/companies/CompanyCard.tsx create mode 100644 components/companies/CompanyProfile.tsx create mode 100644 components/companies/CompanyRegistrationForm.tsx create mode 100644 components/companies/README.md create mode 100644 components/companies/VerificationBadge.tsx create mode 100644 components/companies/index.ts create mode 100644 components/dashboard/AnalyticsCharts.tsx create mode 100644 components/dashboard/CompanyDashboard.tsx create mode 100644 components/dashboard/CompanyHeader.tsx create mode 100644 components/dashboard/CompanySidebar.tsx create mode 100644 components/dashboard/EventForm.tsx create mode 100644 components/dashboard/TeamManagement.tsx create mode 100644 components/help/CompanyFAQ.tsx create mode 100644 components/help/HelpTooltip.tsx create mode 100644 components/moderation/EventReview.tsx create mode 100644 components/moderation/ModerationQueue.tsx create mode 100644 components/moderation/README.md create mode 100644 components/notifications/NotificationCenter.tsx create mode 100644 components/notifications/NotificationItem.tsx create mode 100644 components/notifications/NotificationPreferences.tsx create mode 100644 components/notifications/README.md create mode 100644 components/notifications/index.ts create mode 100644 components/notifications/notification-utils.tsx create mode 100644 components/subscription/README.md create mode 100644 components/subscription/SubscriptionExpiryWarning.tsx create mode 100644 components/subscription/SubscriptionManagement.tsx create mode 100644 components/subscription/UpgradePrompt.tsx create mode 100644 components/subscription/index.ts create mode 100644 components/ui/breadcrumb.tsx create mode 100644 components/ui/calendar.tsx create mode 100644 components/ui/popover.tsx create mode 100644 components/ui/scroll-area.tsx create mode 100644 contexts/CompanyContext.tsx create mode 100644 docs/README.md create mode 100644 hooks/useAnalyticsTracking.ts create mode 100644 hooks/useCompany.ts create mode 100644 hooks/useCompanyAnalytics.ts create mode 100644 hooks/useCompanyEvents.ts create mode 100644 hooks/useCompanyMembers.ts create mode 100644 hooks/useModerationQueue.ts create mode 100644 hooks/useNotifications.ts create mode 100644 hooks/useSubscription.ts create mode 100644 lib/email/company-emails.ts create mode 100644 lib/email/templates/README.md create mode 100644 lib/email/templates/company-registration-confirmation.tsx create mode 100644 lib/email/templates/company-verification-approved.tsx create mode 100644 lib/email/templates/company-verification-rejected.tsx create mode 100644 lib/email/templates/event-approved.tsx create mode 100644 lib/email/templates/event-rejected.tsx create mode 100644 lib/email/templates/event-submitted.tsx create mode 100644 lib/email/templates/index.ts create mode 100644 lib/email/templates/new-registration-notification.tsx create mode 100644 lib/email/templates/team-invitation.tsx create mode 100644 lib/services/analytics-service.ts create mode 100644 lib/services/authorization-service.ts create mode 100644 lib/services/company-member-service.ts create mode 100644 lib/services/company-service.ts create mode 100644 lib/services/moderation-service.ts create mode 100644 lib/services/notification-service.ts create mode 100644 lib/services/subscription-service.ts create mode 100644 lib/storage/README.md create mode 100644 lib/storage/company-assets.ts create mode 100644 lib/storage/company-documents.ts create mode 100644 lib/storage/index.ts create mode 100644 scripts/check-admin-status.js create mode 100644 scripts/migrate-codeunia-company.js create mode 100644 scripts/setup-test-users.js create mode 100644 scripts/test-migration.js create mode 100644 types/company.ts create mode 100644 types/notifications.ts 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 + + +
+ +