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
	 tony
					tony