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:
tony
2025-09-17 22:01:07 +10:00
parent 074a82bfb6
commit 2e1bb2e55e
4018 changed files with 7539 additions and 38906 deletions

View 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 }
)
}
}

View 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 }
)
}
}

View 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;
}
}

View 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>
)
}

View 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>
)
}