647 lines
24 KiB
TypeScript
647 lines
24 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect, useCallback } from 'react'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select'
|
|
import { Input } from '@/components/ui/input'
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '@/components/ui/table'
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog'
|
|
import {
|
|
Mail,
|
|
Search,
|
|
Filter,
|
|
Eye,
|
|
Calendar,
|
|
User,
|
|
Building,
|
|
Phone,
|
|
MessageSquare,
|
|
ExternalLink
|
|
} from 'lucide-react'
|
|
import { toast } from 'sonner'
|
|
|
|
interface FormResponse {
|
|
id: string
|
|
formId: string
|
|
data: any // Changed from specific interface to any to handle different form types
|
|
metadata?: {
|
|
ipAddress?: string
|
|
userAgent?: string
|
|
referrer?: string
|
|
timestamp?: string
|
|
}
|
|
status: string
|
|
createdAt: string
|
|
updatedAt: string
|
|
}
|
|
|
|
interface Pagination {
|
|
page: number
|
|
limit: number
|
|
total: number
|
|
pages: number
|
|
}
|
|
|
|
export default function AdminFormsPage() {
|
|
const [forms, setForms] = useState<FormResponse[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [selectedForm, setSelectedForm] = useState<FormResponse | null>(null)
|
|
const [showDetails, setShowDetails] = useState(false)
|
|
const [pagination, setPagination] = useState<Pagination>({
|
|
page: 1,
|
|
limit: 10,
|
|
total: 0,
|
|
pages: 0
|
|
})
|
|
|
|
// Filters
|
|
const [statusFilter, setStatusFilter] = useState('all')
|
|
const [formTypeFilter, setFormTypeFilter] = useState('all')
|
|
const [searchQuery, setSearchQuery] = useState('')
|
|
|
|
const fetchForms = useCallback(async () => {
|
|
try {
|
|
setLoading(true)
|
|
const params = new URLSearchParams({
|
|
page: pagination.page.toString(),
|
|
limit: pagination.limit.toString(),
|
|
})
|
|
|
|
if (statusFilter !== 'all') {
|
|
params.append('status', statusFilter)
|
|
}
|
|
|
|
if (formTypeFilter !== 'all') {
|
|
params.append('formId', formTypeFilter)
|
|
}
|
|
|
|
const response = await fetch(`/api/forms?${params}`)
|
|
const result = await response.json()
|
|
|
|
if (result.success) {
|
|
setForms(result.data)
|
|
setPagination(result.pagination)
|
|
} else {
|
|
toast.error('Failed to fetch form responses')
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching forms:', error)
|
|
toast.error('Failed to fetch form responses')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}, [pagination.page, pagination.limit, statusFilter, formTypeFilter])
|
|
|
|
useEffect(() => {
|
|
fetchForms()
|
|
}, [fetchForms])
|
|
|
|
const updateFormStatus = async (formId: string, newStatus: string) => {
|
|
try {
|
|
const response = await fetch(`/api/forms/${formId}`, {
|
|
method: 'PATCH',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ status: newStatus })
|
|
})
|
|
|
|
if (response.ok) {
|
|
toast.success('Status updated successfully')
|
|
fetchForms()
|
|
if (selectedForm && selectedForm.id === formId) {
|
|
setSelectedForm({ ...selectedForm, status: newStatus })
|
|
}
|
|
} else {
|
|
toast.error('Failed to update status')
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating status:', error)
|
|
toast.error('Failed to update status')
|
|
}
|
|
}
|
|
|
|
const getStatusBadge = (status: string) => {
|
|
const statusColors = {
|
|
new: 'bg-blue-100 text-blue-800',
|
|
in_progress: 'bg-yellow-100 text-yellow-800',
|
|
resolved: 'bg-green-100 text-green-800',
|
|
closed: 'bg-gray-100 text-gray-800'
|
|
}
|
|
|
|
return (
|
|
<Badge className={statusColors[status as keyof typeof statusColors] || 'bg-gray-100 text-gray-800'}>
|
|
{status.replace('_', ' ').toUpperCase()}
|
|
</Badge>
|
|
)
|
|
}
|
|
|
|
const getFormTypeBadge = (formId: string) => {
|
|
const typeColors = {
|
|
contact: 'bg-purple-100 text-purple-800',
|
|
partnership: 'bg-indigo-100 text-indigo-800',
|
|
bulk_inquiry: 'bg-orange-100 text-orange-800',
|
|
support: 'bg-teal-100 text-teal-800'
|
|
}
|
|
|
|
return (
|
|
<Badge variant="outline" className={typeColors[formId as keyof typeof typeColors]}>
|
|
{formId.replace('_', ' ').toUpperCase()}
|
|
</Badge>
|
|
)
|
|
}
|
|
|
|
const filteredForms = forms.filter(form => {
|
|
if (searchQuery) {
|
|
const query = searchQuery.toLowerCase()
|
|
const data = form.data
|
|
|
|
// Handle different form types
|
|
if (form.formId === 'partnership_application') {
|
|
return (
|
|
data.firstName?.toLowerCase().includes(query) ||
|
|
data.lastName?.toLowerCase().includes(query) ||
|
|
data.email?.toLowerCase().includes(query) ||
|
|
data.businessName?.toLowerCase().includes(query) ||
|
|
data.partnershipTier?.toLowerCase().includes(query) ||
|
|
data.motivation?.toLowerCase().includes(query)
|
|
)
|
|
} else {
|
|
// Contact forms and other types
|
|
return (
|
|
data.name?.toLowerCase().includes(query) ||
|
|
data.email?.toLowerCase().includes(query) ||
|
|
data.message?.toLowerCase().includes(query) ||
|
|
data.subject?.toLowerCase().includes(query)
|
|
)
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-3xl font-bold tracking-tight">Form Responses</h1>
|
|
<p className="text-muted-foreground">
|
|
Manage and respond to customer inquiries and form submissions
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Filter className="h-5 w-5" />
|
|
Filters
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="flex flex-wrap gap-4">
|
|
<div className="flex items-center gap-2">
|
|
<Search className="h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
placeholder="Search by name, email, or message..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
className="w-64"
|
|
/>
|
|
</div>
|
|
|
|
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
|
<SelectTrigger className="w-40">
|
|
<SelectValue placeholder="Status" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">All Statuses</SelectItem>
|
|
<SelectItem value="new">New</SelectItem>
|
|
<SelectItem value="in_progress">In Progress</SelectItem>
|
|
<SelectItem value="resolved">Resolved</SelectItem>
|
|
<SelectItem value="closed">Closed</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
<Select value={formTypeFilter} onValueChange={setFormTypeFilter}>
|
|
<SelectTrigger className="w-40">
|
|
<SelectValue placeholder="Form Type" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">All Types</SelectItem>
|
|
<SelectItem value="contact">Contact</SelectItem>
|
|
<SelectItem value="partnership">Partnership</SelectItem>
|
|
<SelectItem value="bulk_inquiry">Bulk Inquiry</SelectItem>
|
|
<SelectItem value="support">Support</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Form Responses Table */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>
|
|
Form Responses ({pagination.total})
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Click on any row to view detailed information and respond to inquiries
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{loading ? (
|
|
<div className="flex justify-center py-8">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Name</TableHead>
|
|
<TableHead>Email</TableHead>
|
|
<TableHead>Type</TableHead>
|
|
<TableHead>Status</TableHead>
|
|
<TableHead>Subject</TableHead>
|
|
<TableHead>Date</TableHead>
|
|
<TableHead>Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{filteredForms.map((form) => {
|
|
// Handle different form types for display
|
|
const displayName = form.formId === 'partnership_application'
|
|
? `${form.data.firstName || ''} ${form.data.lastName || ''}`.trim()
|
|
: form.data.name || 'Unknown'
|
|
|
|
const displayEmail = form.data.email || 'No email'
|
|
|
|
const displaySubject = form.formId === 'partnership_application'
|
|
? `${form.data.partnershipTier || 'Unknown'} Partnership Application`
|
|
: form.data.subject || form.data.inquiryType || 'No subject'
|
|
|
|
return (
|
|
<TableRow key={form.id} className="cursor-pointer hover:bg-muted/50">
|
|
<TableCell className="font-medium">{displayName}</TableCell>
|
|
<TableCell>{displayEmail}</TableCell>
|
|
<TableCell>{getFormTypeBadge(form.formId)}</TableCell>
|
|
<TableCell>{getStatusBadge(form.status)}</TableCell>
|
|
<TableCell className="max-w-xs truncate">
|
|
{displaySubject}
|
|
</TableCell>
|
|
<TableCell>
|
|
{new Date(form.createdAt).toLocaleDateString()}
|
|
</TableCell>
|
|
<TableCell>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => {
|
|
setSelectedForm(form)
|
|
setShowDetails(true)
|
|
}}
|
|
>
|
|
<Eye className="h-4 w-4" />
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
)
|
|
})}
|
|
|
|
</TableBody>
|
|
</Table>
|
|
|
|
{/* Pagination */}
|
|
{pagination.pages > 1 && (
|
|
<div className="flex items-center justify-between">
|
|
<p className="text-sm text-muted-foreground">
|
|
Showing {((pagination.page - 1) * pagination.limit) + 1} to{' '}
|
|
{Math.min(pagination.page * pagination.limit, pagination.total)} of{' '}
|
|
{pagination.total} entries
|
|
</p>
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setPagination(p => ({ ...p, page: p.page - 1 }))}
|
|
disabled={pagination.page === 1}
|
|
>
|
|
Previous
|
|
</Button>
|
|
<span className="text-sm">
|
|
Page {pagination.page} of {pagination.pages}
|
|
</span>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setPagination(p => ({ ...p, page: p.page + 1 }))}
|
|
disabled={pagination.page === pagination.pages}
|
|
>
|
|
Next
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Form Details Dialog */}
|
|
<Dialog open={showDetails} onOpenChange={setShowDetails}>
|
|
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-2">
|
|
<Mail className="h-5 w-5" />
|
|
Form Response Details
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
View and manage this form submission
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
{selectedForm && (
|
|
<div className="space-y-6">
|
|
{/* Header Info */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-1">
|
|
<div className="flex items-center gap-2">
|
|
{getFormTypeBadge(selectedForm.formId)}
|
|
{getStatusBadge(selectedForm.status)}
|
|
</div>
|
|
<p className="text-sm text-muted-foreground">
|
|
Submitted on {new Date(selectedForm.createdAt).toLocaleDateString()} at{' '}
|
|
{new Date(selectedForm.createdAt).toLocaleTimeString()}
|
|
</p>
|
|
</div>
|
|
|
|
<Select
|
|
value={selectedForm.status}
|
|
onValueChange={(newStatus) => updateFormStatus(selectedForm.id, newStatus)}
|
|
>
|
|
<SelectTrigger className="w-40">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="new">New</SelectItem>
|
|
<SelectItem value="in_progress">In Progress</SelectItem>
|
|
<SelectItem value="resolved">Resolved</SelectItem>
|
|
<SelectItem value="closed">Closed</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Contact Information */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-lg">Contact Information</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
<div className="flex items-center gap-2">
|
|
<User className="h-4 w-4 text-muted-foreground" />
|
|
<span className="font-medium">Name:</span>
|
|
<span>
|
|
{selectedForm.formId === 'partnership_application'
|
|
? `${selectedForm.data.firstName || ''} ${selectedForm.data.lastName || ''}`.trim()
|
|
: selectedForm.data.name || 'Unknown'
|
|
}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<Mail className="h-4 w-4 text-muted-foreground" />
|
|
<span className="font-medium">Email:</span>
|
|
<a
|
|
href={`mailto:${selectedForm.data.email}`}
|
|
className="text-blue-600 hover:underline flex items-center gap-1"
|
|
>
|
|
{selectedForm.data.email}
|
|
<ExternalLink className="h-3 w-3" />
|
|
</a>
|
|
</div>
|
|
|
|
{selectedForm.data.phone && (
|
|
<div className="flex items-center gap-2">
|
|
<Phone className="h-4 w-4 text-muted-foreground" />
|
|
<span className="font-medium">Phone:</span>
|
|
<a
|
|
href={`tel:${selectedForm.data.phone}`}
|
|
className="text-blue-600 hover:underline"
|
|
>
|
|
{selectedForm.data.phone}
|
|
</a>
|
|
</div>
|
|
)}
|
|
|
|
{/* Show business info for partnership applications */}
|
|
{selectedForm.formId === 'partnership_application' && selectedForm.data.businessName && (
|
|
<div className="flex items-center gap-2">
|
|
<Building className="h-4 w-4 text-muted-foreground" />
|
|
<span className="font-medium">Business:</span>
|
|
<span>{selectedForm.data.businessName}</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Show company info for other forms */}
|
|
{selectedForm.formId !== 'partnership_application' && selectedForm.data.company && (
|
|
<div className="flex items-center gap-2">
|
|
<Building className="h-4 w-4 text-muted-foreground" />
|
|
<span className="font-medium">Company:</span>
|
|
<span>{selectedForm.data.company}</span>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Message/Details Content */}
|
|
{selectedForm.formId === 'partnership_application' ? (
|
|
// Partnership Application Details
|
|
<>
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-lg">Partnership Details</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<span className="font-medium">Partnership Tier:</span>
|
|
<Badge variant="outline" className="ml-2">{selectedForm.data.partnershipTier}</Badge>
|
|
</div>
|
|
<div>
|
|
<span className="font-medium">Expected Customers:</span>
|
|
<span className="ml-2">{selectedForm.data.expectedCustomers || 'Not specified'}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<span className="font-medium">Business Type:</span>
|
|
<span className="ml-2">{selectedForm.data.businessType || 'Not specified'}</span>
|
|
</div>
|
|
<div>
|
|
<span className="font-medium">Experience:</span>
|
|
<span className="ml-2">{selectedForm.data.experience || 'Not specified'}</span>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-lg">Address Information</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-2">
|
|
<div><span className="font-medium">Address:</span> {selectedForm.data.address || 'Not provided'}</div>
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<div><span className="font-medium">City:</span> {selectedForm.data.city || 'Not provided'}</div>
|
|
<div><span className="font-medium">State:</span> {selectedForm.data.state || 'Not provided'}</div>
|
|
<div><span className="font-medium">Zip Code:</span> {selectedForm.data.zipCode || 'Not provided'}</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-lg">Motivation & Plans</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div>
|
|
<span className="font-medium">Motivation:</span>
|
|
<div className="bg-muted/50 p-3 rounded-lg mt-2">
|
|
<p className="whitespace-pre-wrap">{selectedForm.data.motivation || 'Not provided'}</p>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<span className="font-medium">Marketing Plan:</span>
|
|
<div className="bg-muted/50 p-3 rounded-lg mt-2">
|
|
<p className="whitespace-pre-wrap">{selectedForm.data.marketingPlan || 'Not provided'}</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</>
|
|
) : (
|
|
// Regular Contact Form Message
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-lg">Message</CardTitle>
|
|
{selectedForm.data.subject && (
|
|
<CardDescription className="font-medium">
|
|
Subject: {selectedForm.data.subject}
|
|
</CardDescription>
|
|
)}
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="bg-muted/50 p-4 rounded-lg">
|
|
<p className="whitespace-pre-wrap">{selectedForm.data.message}</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Additional Information */}
|
|
{(selectedForm.data.inquiryType || selectedForm.data.formSource) && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-lg">Additional Information</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-2">
|
|
{selectedForm.data.inquiryType && (
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-medium">Inquiry Type:</span>
|
|
<Badge variant="outline">{selectedForm.data.inquiryType}</Badge>
|
|
</div>
|
|
)}
|
|
|
|
{selectedForm.data.formSource && (
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-medium">Form Source:</span>
|
|
<span>{selectedForm.data.formSource}</span>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Metadata */}
|
|
{selectedForm.metadata && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-lg">Technical Details</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-2 text-sm">
|
|
{selectedForm.metadata.ipAddress && (
|
|
<div>
|
|
<span className="font-medium">IP Address:</span> {selectedForm.metadata.ipAddress}
|
|
</div>
|
|
)}
|
|
|
|
{selectedForm.metadata.referrer && (
|
|
<div>
|
|
<span className="font-medium">Referrer:</span> {selectedForm.metadata.referrer}
|
|
</div>
|
|
)}
|
|
|
|
{selectedForm.metadata.userAgent && (
|
|
<div>
|
|
<span className="font-medium">User Agent:</span>
|
|
<div className="mt-1 p-2 bg-muted/50 rounded text-xs break-all">
|
|
{selectedForm.metadata.userAgent}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Actions */}
|
|
<div className="flex gap-3 pt-4 border-t">
|
|
<Button
|
|
onClick={() => window.open(`mailto:${selectedForm.data.email}?subject=Re: ${selectedForm.data.subject || 'Your inquiry'}`)}
|
|
className="flex items-center gap-2"
|
|
>
|
|
<Mail className="h-4 w-4" />
|
|
Reply via Email
|
|
</Button>
|
|
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => updateFormStatus(selectedForm.id, 'resolved')}
|
|
disabled={selectedForm.status === 'resolved'}
|
|
>
|
|
Mark as Resolved
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
)
|
|
}
|