Files
chorus-services/modules/dashboard/components/LeadsTable.tsx
tony 2e1bb2e55e Major update to chorus.services platform
- Extensive updates to system configuration and deployment
- Enhanced documentation and architecture improvements
- Updated dependencies and build configurations
- Improved service integrations and workflows

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-17 22:01:07 +10:00

390 lines
16 KiB
TypeScript

'use client'
import { useState, useEffect } from 'react'
import { Search, ChevronLeft, ChevronRight, ArrowUpDown, Eye } from 'lucide-react'
import { Lead } from '@/lib/db'
interface LeadsTableProps {
initialLeads: Lead[]
initialPagination: any
}
export default function LeadsTable({ initialLeads, initialPagination }: LeadsTableProps) {
const [leads, setLeads] = useState<Lead[]>(initialLeads)
const [pagination, setPagination] = useState(initialPagination)
const [loading, setLoading] = useState(false)
const [search, setSearch] = useState('')
const [sortBy, setSortBy] = useState('created_at')
const [sortOrder, setSortOrder] = useState('DESC')
const [selectedLead, setSelectedLead] = useState<Lead | null>(null)
const fetchLeads = async (params: { page?: number; search?: string; sortBy?: string; sortOrder?: string } = {}) => {
setLoading(true)
try {
const searchParams = new URLSearchParams({
page: (params.page || pagination.page).toString(),
search: params.search ?? search,
sortBy: params.sortBy || sortBy,
sortOrder: params.sortOrder || sortOrder,
})
const response = await fetch(`/api/leads?${searchParams}`)
const data = await response.json()
setLeads(data.leads)
setPagination(data.pagination)
} catch (error) {
console.error('Failed to fetch leads:', error)
} finally {
setLoading(false)
}
}
const handleSort = (column: string) => {
const newOrder = sortBy === column && sortOrder === 'ASC' ? 'DESC' : 'ASC'
setSortBy(column)
setSortOrder(newOrder)
fetchLeads({ sortBy: column, sortOrder: newOrder })
}
const handleSearch = (e: React.FormEvent) => {
e.preventDefault()
fetchLeads({ page: 1, search })
}
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('en-AU', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
})
}
return (
<div className="dashboard-card overflow-hidden">
{/* Header */}
<div className="p-chorus-lg border-b border-sand-200 dark:border-carbon-700">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-chorus-md">
<h3 className="text-xl font-semibold text-carbon-900 dark:text-white">
Lead Management
</h3>
<form onSubmit={handleSearch} className="flex gap-chorus-sm">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-carbon-400" />
<input
type="text"
placeholder="Search leads..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-10 pr-4 py-2 border border-sand-300 dark:border-carbon-600 rounded-lg bg-white dark:bg-carbon-800 text-carbon-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-mulberry-500 focus:border-mulberry-500 shadow-sm"
/>
</div>
<button
type="submit"
className="btn-primary px-chorus-md py-2 text-sm"
>
Search
</button>
</form>
</div>
</div>
{/* Table */}
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="bg-sand-100 dark:bg-carbon-800">
<th className="table-header">
<button
onClick={() => handleSort('created_at')}
className="flex items-center gap-1 hover:text-carbon-900 dark:hover:text-white"
>
Date <ArrowUpDown className="h-3 w-3" />
</button>
</th>
<th className="table-header">
<button
onClick={() => handleSort('first_name')}
className="flex items-center gap-1 hover:text-carbon-900 dark:hover:text-white"
>
Name <ArrowUpDown className="h-3 w-3" />
</button>
</th>
<th className="table-header">
<button
onClick={() => handleSort('email')}
className="flex items-center gap-1 hover:text-carbon-900 dark:hover:text-white"
>
Email <ArrowUpDown className="h-3 w-3" />
</button>
</th>
<th className="table-header">
<button
onClick={() => handleSort('company_name')}
className="flex items-center gap-1 hover:text-carbon-900 dark:hover:text-white"
>
Company <ArrowUpDown className="h-3 w-3" />
</button>
</th>
<th className="table-header">
<button
onClick={() => handleSort('lead_source')}
className="flex items-center gap-1 hover:text-carbon-900 dark:hover:text-white"
>
Source <ArrowUpDown className="h-3 w-3" />
</button>
</th>
<th className="table-header">Actions</th>
</tr>
</thead>
<tbody>
{loading ? (
<tr>
<td colSpan={6} className="table-cell text-center py-chorus-xxl">
Loading...
</td>
</tr>
) : leads.length === 0 ? (
<tr>
<td colSpan={6} className="table-cell text-center py-chorus-xxl text-carbon-500">
No leads found
</td>
</tr>
) : (
leads.map((lead) => (
<tr key={lead.id} className="hover:bg-sand-100 dark:hover:bg-carbon-800 transition-colors">
<td className="table-cell">
{formatDate(lead.created_at.toString())}
</td>
<td className="table-cell">
<div>
<div className="font-medium text-carbon-900 dark:text-white">
{lead.first_name} {lead.last_name}
</div>
{lead.company_role && (
<div className="text-xs text-carbon-500 dark:text-carbon-400">
{lead.company_role}
</div>
)}
</div>
</td>
<td className="table-cell">
<a
href={`mailto:${lead.email}`}
className="text-ocean-600 dark:text-ocean-400 hover:underline"
>
{lead.email}
</a>
</td>
<td className="table-cell">
{lead.company_name || '-'}
</td>
<td className="table-cell">
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
lead.lead_source === 'request_early_access'
? 'bg-mulberry-100 text-mulberry-800 dark:bg-mulberry-900 dark:text-mulberry-200'
: 'bg-ocean-100 text-ocean-800 dark:bg-ocean-900 dark:text-ocean-200'
}`}>
{lead.lead_source.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase())}
</span>
</td>
<td className="table-cell">
<button
onClick={() => setSelectedLead(lead)}
className="text-carbon-600 dark:text-carbon-300 hover:text-carbon-900 dark:hover:text-white transition-colors"
>
<Eye className="h-4 w-4" />
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{/* Pagination */}
<div className="p-chorus-lg border-t border-sand-200 dark:border-carbon-700">
<div className="flex items-center justify-between">
<div className="text-sm text-carbon-600 dark:text-carbon-300">
Showing {(pagination.page - 1) * pagination.limit + 1} to{' '}
{Math.min(pagination.page * pagination.limit, pagination.total)} of{' '}
{pagination.total} results
</div>
<div className="flex items-center gap-chorus-sm">
<button
onClick={() => fetchLeads({ page: pagination.page - 1 })}
disabled={!pagination.hasPrev || loading}
className="flex items-center gap-1 px-3 py-2 text-sm border border-sand-300 dark:border-carbon-600 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-sand-100 dark:hover:bg-carbon-800 transition-colors text-carbon-700 dark:text-carbon-200"
>
<ChevronLeft className="h-4 w-4" />
Previous
</button>
<span className="text-sm text-carbon-600 dark:text-carbon-300">
Page {pagination.page} of {pagination.totalPages}
</span>
<button
onClick={() => fetchLeads({ page: pagination.page + 1 })}
disabled={!pagination.hasNext || loading}
className="flex items-center gap-1 px-3 py-2 text-sm border border-sand-300 dark:border-carbon-600 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-sand-100 dark:hover:bg-carbon-800 transition-colors text-carbon-700 dark:text-carbon-200"
>
Next
<ChevronRight className="h-4 w-4" />
</button>
</div>
</div>
</div>
{/* Lead Detail Modal */}
{selectedLead && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-chorus-lg z-50">
<div className="bg-white dark:bg-carbon-900 rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-chorus-lg border-b border-sand-200 dark:border-carbon-700">
<div className="flex justify-between items-center">
<h3 className="text-xl font-semibold text-carbon-900 dark:text-white">
Lead Details
</h3>
<button
onClick={() => setSelectedLead(null)}
className="text-carbon-600 dark:text-carbon-300 hover:text-carbon-900 dark:hover:text-white"
>
</button>
</div>
</div>
<div className="p-chorus-lg space-y-chorus-md">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-chorus-md">
<div>
<label className="block text-sm font-medium text-carbon-600 dark:text-carbon-300 mb-1">
Name
</label>
<p className="text-carbon-900 dark:text-white">
{selectedLead.first_name} {selectedLead.last_name}
</p>
</div>
<div>
<label className="block text-sm font-medium text-carbon-600 dark:text-carbon-300 mb-1">
Email
</label>
<p className="text-carbon-900 dark:text-white">
{selectedLead.email}
</p>
</div>
{selectedLead.company_name && (
<div>
<label className="block text-sm font-medium text-carbon-600 dark:text-carbon-300 mb-1">
Company
</label>
<p className="text-carbon-900 dark:text-white">
{selectedLead.company_name}
</p>
</div>
)}
{selectedLead.company_role && (
<div>
<label className="block text-sm font-medium text-carbon-600 dark:text-carbon-300 mb-1">
Role
</label>
<p className="text-carbon-900 dark:text-white">
{selectedLead.company_role}
</p>
</div>
)}
<div>
<label className="block text-sm font-medium text-carbon-600 dark:text-carbon-300 mb-1">
Lead Source
</label>
<p className="text-carbon-900 dark:text-white">
{selectedLead.lead_source.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase())}
</p>
</div>
<div>
<label className="block text-sm font-medium text-carbon-600 dark:text-carbon-300 mb-1">
Date
</label>
<p className="text-carbon-900 dark:text-white">
{formatDate(selectedLead.created_at.toString())}
</p>
</div>
</div>
{selectedLead.inquiry_details && (
<div>
<label className="block text-sm font-medium text-carbon-600 dark:text-carbon-300 mb-1">
Inquiry Details
</label>
<p className="text-carbon-900 dark:text-white whitespace-pre-wrap">
{selectedLead.inquiry_details}
</p>
</div>
)}
{selectedLead.custom_message && (
<div>
<label className="block text-sm font-medium text-carbon-600 dark:text-carbon-300 mb-1">
Custom Message
</label>
<p className="text-carbon-900 dark:text-white whitespace-pre-wrap">
{selectedLead.custom_message}
</p>
</div>
)}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-chorus-md text-sm">
<div>
<label className="block text-xs font-medium text-carbon-500 dark:text-carbon-400 mb-1">
IP Address
</label>
<p className="text-carbon-700 dark:text-carbon-200">
{selectedLead.ip_address}
</p>
</div>
{selectedLead.country_code && (
<div>
<label className="block text-xs font-medium text-carbon-500 dark:text-carbon-400 mb-1">
Country
</label>
<p className="text-carbon-700 dark:text-carbon-200">
{selectedLead.country_code}
</p>
</div>
)}
<div>
<label className="block text-xs font-medium text-carbon-500 dark:text-carbon-400 mb-1">
GDPR Consent
</label>
<p className={`${selectedLead.gdpr_consent_given ? 'text-eucalyptus-600' : 'text-coral-600'}`}>
{selectedLead.gdpr_consent_given ? 'Given' : 'Not Given'}
</p>
</div>
<div>
<label className="block text-xs font-medium text-carbon-500 dark:text-carbon-400 mb-1">
Marketing Consent
</label>
<p className={`${selectedLead.marketing_consent ? 'text-eucalyptus-600' : 'text-coral-600'}`}>
{selectedLead.marketing_consent ? 'Given' : 'Not Given'}
</p>
</div>
</div>
</div>
</div>
</div>
)}
</div>
)
}