import { NextRequest, NextResponse } from 'next/server'; import { postgresClient } from '@/lib/services/postgres-client'; async function getExcludedCompanyIds(): Promise { try { const result = await postgresClient.query( `SELECT setting_value FROM kiosk_settings WHERE setting_key = 'excluded_company_ids'` ); const value = result.rows[0]?.setting_value || ''; return value ? value.split(',').map((id: string) => parseInt(id.trim())).filter((id: number) => !isNaN(id)) : []; } catch (error) { console.error('Error fetching excluded company IDs:', error); return []; } } async function getExcludedClassifications(): Promise { try { const result = await postgresClient.query( `SELECT setting_value FROM kiosk_settings WHERE setting_key = 'excluded_classifications'` ); const value = result.rows[0]?.setting_value || ''; return value ? value.split(',').map((c: string) => c.trim()).filter(Boolean) : []; } catch (error) { console.error('Error fetching excluded classifications:', error); return []; } } export async function GET(request: NextRequest) { try { // Get excluded company IDs (co-managed clients) const excludedCompanyIds = await getExcludedCompanyIds(); const excludedClassifications = await getExcludedClassifications(); // Build company exclusion filter let excludeCompanyFilter = ''; if (excludedCompanyIds.length > 0 || excludedClassifications.length > 0) { const conditions = []; if (excludedCompanyIds.length > 0) { conditions.push(`company_id NOT IN (${excludedCompanyIds.join(',')})`); } if (excludedClassifications.length > 0) { const classificationList = excludedClassifications.map(c => `'${c.replace(/'/g, "''")}'`).join(','); conditions.push(`company_id NOT IN (SELECT id FROM companies WHERE classification IN (${classificationList}))`); } excludeCompanyFilter = `AND (${conditions.join(' AND ')})`; } // Critical tickets (Priority 1-3) - exclude RMM alerts (source = 8), co-managed clients, and excluded classifications const criticalTicketsResult = await postgresClient.query( `SELECT COUNT(*) as count FROM tickets WHERE completed_date IS NULL AND priority <= 3 AND (source IS NULL OR source != 8) ${excludeCompanyFilter}` ); // Top 3 critical tickets const topCriticalResult = await postgresClient.query( `SELECT t.ticket_number, t.title, c.company_name, t.priority FROM tickets t LEFT JOIN companies c ON t.company_id = c.id WHERE t.completed_date IS NULL AND t.priority <= 3 AND (t.source IS NULL OR t.source != 8) ${excludeCompanyFilter} ORDER BY t.priority ASC, t.create_date ASC LIMIT 3` ); // Tickets waiting engagement (specific statuses) - exclude RMM alerts const waitingTicketsResult = await postgresClient.query( `SELECT COUNT(*) as count FROM tickets WHERE completed_date IS NULL AND status IN (21, 9, 19) AND (source IS NULL OR source != 8) ${excludeCompanyFilter}` ); // Top 3 waiting tickets const topWaitingResult = await postgresClient.query( `SELECT t.ticket_number, t.title, c.company_name, t.last_activity_date FROM tickets t LEFT JOIN companies c ON t.company_id = c.id WHERE t.completed_date IS NULL AND t.status IN (21, 9, 19) AND (t.source IS NULL OR t.source != 8) ${excludeCompanyFilter} ORDER BY t.last_activity_date ASC NULLS FIRST LIMIT 3` ); // Stale tickets (no activity in 7+ days) - exclude RMM alerts const staleTicketsResult = await postgresClient.query( `SELECT COUNT(*) as count FROM tickets WHERE completed_date IS NULL AND last_activity_date < NOW() - INTERVAL '7 days' AND (source IS NULL OR source != 8) ${excludeCompanyFilter}` ); // Top 3 stale tickets (oldest activity first) const topStaleResult = await postgresClient.query( `SELECT t.ticket_number, t.title, c.company_name, t.last_activity_date FROM tickets t LEFT JOIN companies c ON t.company_id = c.id WHERE t.completed_date IS NULL AND t.last_activity_date < NOW() - INTERVAL '7 days' AND (t.source IS NULL OR t.source != 8) ${excludeCompanyFilter} ORDER BY t.last_activity_date ASC NULLS FIRST LIMIT 3` ); // Overdue tickets - exclude RMM alerts const overdueTicketsResult = await postgresClient.query( `SELECT COUNT(*) as count FROM tickets WHERE completed_date IS NULL AND due_date_time < NOW() AND (source IS NULL OR source != 8) ${excludeCompanyFilter}` ); // Top 3 overdue tickets (most overdue first) const topOverdueResult = await postgresClient.query( `SELECT t.ticket_number, t.title, c.company_name, t.due_date_time FROM tickets t LEFT JOIN companies c ON t.company_id = c.id WHERE t.completed_date IS NULL AND t.due_date_time < NOW() AND (t.source IS NULL OR t.source != 8) ${excludeCompanyFilter} ORDER BY t.due_date_time ASC LIMIT 3` ); // Total open tickets - exclude RMM alerts const openTicketsResult = await postgresClient.query( `SELECT COUNT(*) as count FROM tickets WHERE completed_date IS NULL AND (source IS NULL OR source != 8) ${excludeCompanyFilter}` ); // Tickets closed today - exclude RMM alerts const closedTodayResult = await postgresClient.query( `SELECT COUNT(*) as count FROM tickets WHERE DATE(completed_date) = CURRENT_DATE AND (source IS NULL OR source != 8) ${excludeCompanyFilter}` ); // Top 3 recently closed tickets const topClosedResult = await postgresClient.query( `SELECT t.ticket_number, t.title, c.company_name, t.completed_date FROM tickets t LEFT JOIN companies c ON t.company_id = c.id WHERE DATE(t.completed_date) = CURRENT_DATE AND (t.source IS NULL OR t.source != 8) ${excludeCompanyFilter} ORDER BY t.completed_date DESC LIMIT 3` ); // Average resolution time (last 30 days) - exclude RMM alerts const avgResolutionResult = await postgresClient.query( `SELECT AVG(EXTRACT(EPOCH FROM (completed_date - create_date))/3600) as avg_hours FROM tickets WHERE completed_date >= NOW() - INTERVAL '30 days' AND completed_date IS NOT NULL AND (source IS NULL OR source != 8) ${excludeCompanyFilter}` ); // Time entries this week const timeEntriesResult = await postgresClient.query( `SELECT COALESCE(SUM(hours_worked), 0) as hours FROM time_entries WHERE entry_date >= DATE_TRUNC('week', CURRENT_DATE)` ); // Active companies const companiesResult = await postgresClient.query( `SELECT COUNT(*) as count FROM companies WHERE is_active = true AND company_type = 1` ); // Quotes (if table exists) let quotesOpen = 0; try { const quotesResult = await postgresClient.query( `SELECT COUNT(*) as count FROM quotes WHERE status = 'open'` ); quotesOpen = parseInt(quotesResult.rows[0]?.count || '0'); } catch { // Quotes table may not exist } // NMS Coverage let nmsPercentage = 0; try { const nmsResult = await postgresClient.query( `SELECT (SELECT COUNT(*) FROM auvik_tenant_mappings WHERE autotask_company_id IS NOT NULL) as mapped, (SELECT COUNT(*) FROM auvik_tenants) as total` ); const mapped = parseInt(nmsResult.rows[0]?.mapped || '0'); const total = parseInt(nmsResult.rows[0]?.total || '0'); nmsPercentage = total > 0 ? Math.round((mapped / total) * 100) : 0; } catch { // Tables may not exist } // RMM Coverage let rmmPercentage = 0; try { const rmmResult = await postgresClient.query( `SELECT (SELECT COUNT(*) FROM rmm_site_mappings WHERE company_id IS NOT NULL) as mapped, (SELECT COUNT(*) FROM rmm_sites) as total` ); const mapped = parseInt(rmmResult.rows[0]?.mapped || '0'); const total = parseInt(rmmResult.rows[0]?.total || '0'); rmmPercentage = total > 0 ? Math.round((mapped / total) * 100) : 0; } catch { // Tables may not exist } const stats = { criticalTickets: parseInt(criticalTicketsResult.rows[0]?.count || '0'), topCritical: topCriticalResult.rows.map((r: any) => ({ ticketNumber: r.ticket_number, title: r.title, companyName: r.company_name, })), waitingTickets: parseInt(waitingTicketsResult.rows[0]?.count || '0'), topWaiting: topWaitingResult.rows.map((r: any) => ({ ticketNumber: r.ticket_number, title: r.title, companyName: r.company_name, })), staleTickets: parseInt(staleTicketsResult.rows[0]?.count || '0'), topStale: topStaleResult.rows.map((r: any) => ({ ticketNumber: r.ticket_number, title: r.title, companyName: r.company_name, })), overdueTickets: parseInt(overdueTicketsResult.rows[0]?.count || '0'), topOverdue: topOverdueResult.rows.map((r: any) => ({ ticketNumber: r.ticket_number, title: r.title, companyName: r.company_name, })), openTickets: parseInt(openTicketsResult.rows[0]?.count || '0'), closedToday: parseInt(closedTodayResult.rows[0]?.count || '0'), topClosed: topClosedResult.rows.map((r: any) => ({ ticketNumber: r.ticket_number, title: r.title, companyName: r.company_name, })), avgResolutionHours: parseFloat(avgResolutionResult.rows[0]?.avg_hours || '0'), hoursThisWeek: parseFloat(timeEntriesResult.rows[0]?.hours || '0'), activeCompanies: parseInt(companiesResult.rows[0]?.count || '0'), openQuotes: quotesOpen, nmsCoverage: nmsPercentage, rmmCoverage: rmmPercentage, }; return NextResponse.json(stats); } catch (error) { console.error('Error fetching kiosk stats:', error); return NextResponse.json( { error: 'Failed to fetch kiosk stats' }, { status: 500 } ); } }