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>
This commit is contained in:
63
modules/dashboard/app/api/leads/route.ts
Normal file
63
modules/dashboard/app/api/leads/route.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { pool, Lead } from '@/lib/db'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const page = parseInt(searchParams.get('page') || '1')
|
||||
const limit = parseInt(searchParams.get('limit') || '50')
|
||||
const sortBy = searchParams.get('sortBy') || 'created_at'
|
||||
const sortOrder = searchParams.get('sortOrder') || 'DESC'
|
||||
const search = searchParams.get('search') || ''
|
||||
|
||||
const offset = (page - 1) * limit
|
||||
|
||||
let whereClause = ''
|
||||
let queryParams: any[] = [limit, offset]
|
||||
|
||||
if (search) {
|
||||
whereClause = `WHERE (first_name ILIKE $3 OR last_name ILIKE $3 OR email ILIKE $3 OR company_name ILIKE $3)`
|
||||
queryParams.push(`%${search}%`)
|
||||
}
|
||||
|
||||
const validSortColumns = ['created_at', 'first_name', 'last_name', 'email', 'company_name', 'lead_source']
|
||||
const sortColumn = validSortColumns.includes(sortBy) ? sortBy : 'created_at'
|
||||
const order = sortOrder.toUpperCase() === 'ASC' ? 'ASC' : 'DESC'
|
||||
|
||||
const leadsQuery = `
|
||||
SELECT * FROM leads
|
||||
${whereClause}
|
||||
ORDER BY ${sortColumn} ${order}
|
||||
LIMIT $1 OFFSET $2
|
||||
`
|
||||
|
||||
const countQuery = `SELECT COUNT(*) as total FROM leads ${whereClause}`
|
||||
|
||||
const [leadsResult, countResult] = await Promise.all([
|
||||
pool.query(leadsQuery, queryParams),
|
||||
pool.query(countQuery, search ? [`%${search}%`] : [])
|
||||
])
|
||||
|
||||
const leads: Lead[] = leadsResult.rows
|
||||
const total = parseInt(countResult.rows[0].total)
|
||||
const totalPages = Math.ceil(total / limit)
|
||||
|
||||
return NextResponse.json({
|
||||
leads,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages,
|
||||
hasNext: page < totalPages,
|
||||
hasPrev: page > 1
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching leads:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch leads' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
64
modules/dashboard/app/api/stats/route.ts
Normal file
64
modules/dashboard/app/api/stats/route.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { pool, LeadStats } from '@/lib/db'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const queries = [
|
||||
// Total leads
|
||||
'SELECT COUNT(*) as count FROM leads',
|
||||
|
||||
// Leads today
|
||||
'SELECT COUNT(*) as count FROM leads WHERE created_at >= CURRENT_DATE',
|
||||
|
||||
// Leads this week
|
||||
'SELECT COUNT(*) as count FROM leads WHERE created_at >= date_trunc(\'week\', CURRENT_DATE)',
|
||||
|
||||
// Leads this month
|
||||
'SELECT COUNT(*) as count FROM leads WHERE created_at >= date_trunc(\'month\', CURRENT_DATE)',
|
||||
|
||||
// By source
|
||||
'SELECT lead_source, COUNT(*) as count FROM leads GROUP BY lead_source ORDER BY count DESC',
|
||||
|
||||
// By date (last 30 days)
|
||||
`SELECT
|
||||
DATE(created_at) as date,
|
||||
COUNT(*) as count
|
||||
FROM leads
|
||||
WHERE created_at >= CURRENT_DATE - INTERVAL '30 days'
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY date DESC`
|
||||
]
|
||||
|
||||
const [
|
||||
totalResult,
|
||||
todayResult,
|
||||
weekResult,
|
||||
monthResult,
|
||||
sourceResult,
|
||||
dateResult
|
||||
] = await Promise.all(queries.map(query => pool.query(query)))
|
||||
|
||||
const stats: LeadStats = {
|
||||
total_leads: parseInt(totalResult.rows[0].count),
|
||||
leads_today: parseInt(todayResult.rows[0].count),
|
||||
leads_this_week: parseInt(weekResult.rows[0].count),
|
||||
leads_this_month: parseInt(monthResult.rows[0].count),
|
||||
by_source: sourceResult.rows.map(row => ({
|
||||
lead_source: row.lead_source,
|
||||
count: parseInt(row.count)
|
||||
})),
|
||||
by_date: dateResult.rows.map(row => ({
|
||||
date: row.date,
|
||||
count: parseInt(row.count)
|
||||
}))
|
||||
}
|
||||
|
||||
return NextResponse.json(stats)
|
||||
} catch (error) {
|
||||
console.error('Error fetching stats:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch statistics' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
39
modules/dashboard/app/globals.css
Normal file
39
modules/dashboard/app/globals.css
Normal file
@@ -0,0 +1,39 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer components {
|
||||
.btn-primary {
|
||||
@apply bg-mulberry-600 hover:bg-mulberry-700 text-white font-semibold rounded-lg shadow-lg hover:shadow-xl transition-all duration-300 ease-out hover:scale-105 active:scale-95;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply border-2 border-mulberry-600 text-mulberry-600 hover:bg-mulberry-600 hover:text-white font-semibold rounded-lg shadow-lg hover:shadow-xl transition-all duration-300 ease-out hover:scale-105 active:scale-95;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
@apply bg-coral-600 hover:bg-coral-700 text-white font-semibold rounded-lg shadow-lg hover:shadow-xl transition-all duration-300 ease-out hover:scale-105 active:scale-95;
|
||||
}
|
||||
|
||||
.table-cell {
|
||||
@apply px-chorus-lg py-chorus-md text-sm border-b border-sand-200 dark:border-carbon-700 text-carbon-900 dark:text-carbon-100;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
@apply px-chorus-lg py-chorus-lg text-xs font-semibold uppercase tracking-wide text-carbon-700 dark:text-carbon-300 bg-sand-100 dark:bg-carbon-800 border-b border-sand-300 dark:border-carbon-600;
|
||||
}
|
||||
|
||||
/* Enhanced card styling for light mode branding */
|
||||
.dashboard-card {
|
||||
@apply bg-white dark:bg-carbon-900 rounded-lg shadow-lg border border-sand-200 dark:border-carbon-700 backdrop-blur-sm;
|
||||
}
|
||||
|
||||
/* Branded gradients */
|
||||
.gradient-primary {
|
||||
@apply bg-gradient-to-r from-mulberry-500 to-mulberry-700;
|
||||
}
|
||||
|
||||
.gradient-accent {
|
||||
@apply bg-gradient-to-r from-ocean-500 to-eucalyptus-500;
|
||||
}
|
||||
}
|
||||
36
modules/dashboard/app/layout.tsx
Normal file
36
modules/dashboard/app/layout.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { Inter, Exo } from 'next/font/google'
|
||||
import './globals.css'
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ['latin'],
|
||||
variable: '--font-inter',
|
||||
display: 'swap',
|
||||
weight: ['100', '200', '300', '400', '500', '600', '700', '800', '900'],
|
||||
})
|
||||
|
||||
const exo = Exo({
|
||||
subsets: ['latin'],
|
||||
variable: '--font-exo',
|
||||
display: 'swap',
|
||||
weight: ['100', '200', '300', '400', '500', '600', '700', '800', '900'],
|
||||
})
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'CHORUS Dashboard',
|
||||
description: 'Lead management dashboard for CHORUS Services',
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" className={`${inter.variable} ${exo.variable}`}>
|
||||
<body className="font-sans antialiased bg-white dark:bg-carbon-950 text-carbon-950 dark:text-white">
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
135
modules/dashboard/app/page.tsx
Normal file
135
modules/dashboard/app/page.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { LeadStats, Lead } from '@/lib/db'
|
||||
import StatsCards from '@/components/StatsCards'
|
||||
import LeadsTable from '@/components/LeadsTable'
|
||||
import ThemeToggle from '@/components/ThemeToggle'
|
||||
import LeadsChart from '@/components/LeadsChart'
|
||||
|
||||
export default function DashboardPage() {
|
||||
const [stats, setStats] = useState<LeadStats | null>(null)
|
||||
const [leadsData, setLeadsData] = useState<{ leads: Lead[], pagination: any } | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const [statsResponse, leadsResponse] = await Promise.all([
|
||||
fetch('/api/stats'),
|
||||
fetch('/api/leads')
|
||||
])
|
||||
|
||||
if (!statsResponse.ok || !leadsResponse.ok) {
|
||||
throw new Error('Failed to fetch data')
|
||||
}
|
||||
|
||||
const statsData = await statsResponse.json()
|
||||
const leadsData = await leadsResponse.json()
|
||||
|
||||
setStats(statsData)
|
||||
setLeadsData(leadsData)
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch dashboard data:', error)
|
||||
setError('Failed to load dashboard data. Please try again.')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<main className="min-h-screen p-chorus-lg flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-16 w-16 border-b-2 border-mulberry-600 mx-auto mb-4"></div>
|
||||
<p className="text-lg text-carbon-600 dark:text-carbon-300">Loading dashboard...</p>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<main className="min-h-screen p-chorus-lg flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<p className="text-lg text-coral-600 mb-4">{error}</p>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="btn-primary px-chorus-lg py-chorus-md"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
if (!stats || !leadsData) {
|
||||
return (
|
||||
<main className="min-h-screen p-chorus-lg flex items-center justify-center">
|
||||
<p className="text-lg text-carbon-600 dark:text-carbon-300">No data available</p>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen p-chorus-lg bg-gradient-to-b from-white via-sand-50 to-sand-100 dark:from-carbon-950 dark:via-carbon-900 dark:to-carbon-950">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Theme Toggle */}
|
||||
<ThemeToggle />
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-chorus-xxl">
|
||||
<h1 className="text-h2 font-logo font-thin text-carbon-950 dark:text-white mb-chorus-md">
|
||||
CHORUS Dashboard
|
||||
</h1>
|
||||
<p className="text-lg text-carbon-600 dark:text-carbon-300">
|
||||
Lead management and analytics for CHORUS Services
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<StatsCards stats={stats} />
|
||||
|
||||
{/* Lead Trends Chart */}
|
||||
<LeadsChart stats={stats} />
|
||||
|
||||
{/* Source Breakdown */}
|
||||
{stats.by_source.length > 0 && (
|
||||
<div className="dashboard-card p-chorus-lg mb-chorus-xxl">
|
||||
<h3 className="text-xl font-semibold text-carbon-900 dark:text-white mb-chorus-lg">
|
||||
Lead Sources
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-chorus-md">
|
||||
{stats.by_source.map((source) => (
|
||||
<div
|
||||
key={source.lead_source}
|
||||
className="bg-sand-100 dark:bg-carbon-800 rounded-lg p-chorus-md border border-sand-200 dark:border-carbon-700 hover:shadow-md transition-shadow duration-200"
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium text-carbon-700 dark:text-carbon-200">
|
||||
{source.lead_source.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase())}
|
||||
</span>
|
||||
<span className="text-lg font-bold text-carbon-900 dark:text-white">
|
||||
{source.count}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Leads Table */}
|
||||
<LeadsTable
|
||||
initialLeads={leadsData.leads}
|
||||
initialPagination={leadsData.pagination}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user