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 is_deleted = false AND priority <= 3 AND (source IS NULL OR source != 8) ${excludeCompanyFilter}` ); // Top 10 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.is_deleted = false AND t.priority <= 3 AND (t.source IS NULL OR t.source != 8) ${excludeCompanyFilter} ORDER BY t.priority ASC, t.create_date ASC LIMIT 10` ); // 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 is_deleted = false AND status IN (21, 9, 19) AND (source IS NULL OR source != 8) ${excludeCompanyFilter}` ); // Top 10 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.is_deleted = false 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 10` ); // 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 is_deleted = false AND last_activity_date < NOW() - INTERVAL '7 days' AND (source IS NULL OR source != 8) ${excludeCompanyFilter}` ); // Top 10 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.is_deleted = false 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 10` ); // Overdue tickets - exclude RMM alerts const overdueTicketsResult = await postgresClient.query( `SELECT COUNT(*) as count FROM tickets WHERE completed_date IS NULL AND is_deleted = false AND due_date_time < NOW() AND (source IS NULL OR source != 8) ${excludeCompanyFilter}` ); // Top 10 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.is_deleted = false AND t.due_date_time < NOW() AND (t.source IS NULL OR t.source != 8) ${excludeCompanyFilter} ORDER BY t.due_date_time ASC LIMIT 10` ); // Total open tickets - exclude RMM alerts const openTicketsResult = await postgresClient.query( `SELECT COUNT(*) as count FROM tickets WHERE completed_date IS NULL AND is_deleted = false 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 is_deleted = false 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.is_deleted = false AND (t.source IS NULL OR t.source != 8) ${excludeCompanyFilter} ORDER BY t.completed_date DESC LIMIT 3` ); // Average closed tickets per day last week const avgClosedLastWeekResult = await postgresClient.query( `SELECT ROUND(COUNT(*)::numeric / 7, 1) as avg_per_day FROM tickets WHERE completed_date >= NOW() - INTERVAL '7 days' AND completed_date < NOW() AND is_deleted = false AND (source IS NULL OR source != 8) ${excludeCompanyFilter}` ); // Average closed tickets per day last month const avgClosedLastMonthResult = await postgresClient.query( `SELECT ROUND(COUNT(*)::numeric / 30, 1) as avg_per_day FROM tickets WHERE completed_date >= NOW() - INTERVAL '30 days' AND completed_date < NOW() AND is_deleted = false AND (source IS NULL OR source != 8) ${excludeCompanyFilter}` ); // 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 is_deleted = false 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)` ); // Top ticket closers (last 30 days) - using last_activity_resource_id, excluding Autotask system user // Only count tickets where the resource has at least 10 minutes (0.167 hours) of time worked const topTicketClosersResult = await postgresClient.query( `SELECT r.first_name || ' ' || r.last_name as resource_name, COUNT(DISTINCT t.id) as tickets_closed FROM tickets t INNER JOIN resources r ON t.last_activity_resource_id = r.id INNER JOIN ( SELECT ticket_id, resource_id, SUM(hours_worked) as total_hours FROM time_entries GROUP BY ticket_id, resource_id HAVING SUM(hours_worked) >= 0.167 ) te ON t.id = te.ticket_id AND r.id = te.resource_id WHERE t.completed_date >= NOW() - INTERVAL '30 days' AND t.completed_date IS NOT NULL AND t.is_deleted = false AND (t.source IS NULL OR t.source != 8) AND r.id != 4 ${excludeCompanyFilter} GROUP BY r.id, r.first_name, r.last_name ORDER BY tickets_closed DESC LIMIT 10` ); // 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 } // Service Desk - Managed (using primary service desk queues, excluding vendor and alerts) const serviceDeskManagedResult = await postgresClient.query( `SELECT COUNT(*) as count FROM tickets t WHERE t.completed_date IS NULL AND t.is_deleted = false AND t.status NOT IN (21, 9, 19) AND t.ticket_type != 4 AND t.queue_id IN (29682833, 29749490) AND (t.ticket_category IS NULL OR t.ticket_category != 171) AND (t.source IS NULL OR t.source != 8) ${excludeCompanyFilter}` ); // Top Issue/Sub-Issue types for Managed Service tickets const topManagedIssueTypesResult = await postgresClient.query( `SELECT COALESCE(it.label, 'Not Set') as issue_type, COALESCE(sit.label, 'Not Set') as sub_issue_type, COUNT(*) as count FROM tickets t LEFT JOIN issue_types it ON t.issue_type = it.value LEFT JOIN sub_issue_types sit ON t.sub_issue_type = sit.value WHERE t.completed_date IS NULL AND t.is_deleted = false AND t.status NOT IN (21, 9, 19) AND t.ticket_type != 4 AND t.queue_id IN (29682833, 29749490) AND (t.ticket_category IS NULL OR t.ticket_category != 171) AND (t.source IS NULL OR t.source != 8) ${excludeCompanyFilter} GROUP BY it.label, sit.label ORDER BY count DESC LIMIT 10` ); // Service Desk - T&M (Level 1/2 Support queues) const serviceDeskTMResult = await postgresClient.query( `SELECT COUNT(*) as count FROM tickets t WHERE t.completed_date IS NULL AND t.is_deleted = false AND t.status NOT IN (21, 9, 19) AND t.ticket_type != 4 AND t.queue_id IN (29853766, 29853700) AND (t.ticket_category IS NULL OR t.ticket_category != 171) AND (t.source IS NULL OR t.source != 8) ${excludeCompanyFilter}` ); // Vendor Service Desk (All) const serviceDeskVendorResult = await postgresClient.query( `SELECT COUNT(*) as count FROM tickets t WHERE t.completed_date IS NULL AND t.is_deleted = false AND t.ticket_category = 171 AND (t.source IS NULL OR t.source != 8) ${excludeCompanyFilter}` ); // Key Accounts - Hynes Industries const hynesTicketsResult = await postgresClient.query( `SELECT COUNT(*) as count FROM tickets t WHERE t.completed_date IS NULL AND t.is_deleted = false AND t.company_id = 29861375 AND (t.source IS NULL OR t.source != 8)` ); // Key Accounts - Seubert and Associates const seubertTicketsResult = await postgresClient.query( `SELECT COUNT(*) as count FROM tickets t WHERE t.completed_date IS NULL AND t.is_deleted = false AND t.company_id = 29683407 AND (t.source IS NULL OR t.source != 8)` ); // Key Accounts - Universal Plastics (both locations) const universalTicketsResult = await postgresClient.query( `SELECT COUNT(*) as count FROM tickets t WHERE t.completed_date IS NULL AND t.is_deleted = false AND t.company_id IN (29861395, 29861424) AND (t.source IS NULL OR t.source != 8)` ); 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'), avgClosedLastWeek: parseFloat(avgClosedLastWeekResult.rows[0]?.avg_per_day || '0'), avgClosedLastMonth: parseFloat(avgClosedLastMonthResult.rows[0]?.avg_per_day || '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, topTicketClosers: topTicketClosersResult.rows.map((r: any) => ({ resourceName: r.resource_name, ticketsClosed: parseInt(r.tickets_closed), })), nmsCoverage: nmsPercentage, rmmCoverage: rmmPercentage, serviceDeskManaged: parseInt(serviceDeskManagedResult.rows[0]?.count || '0'), topManagedIssueTypes: topManagedIssueTypesResult.rows.map((r: any) => ({ issueType: r.issue_type, subIssueType: r.sub_issue_type, count: parseInt(r.count), })), serviceDeskTM: parseInt(serviceDeskTMResult.rows[0]?.count || '0'), serviceDeskVendor: parseInt(serviceDeskVendorResult.rows[0]?.count || '0'), hynesTickets: parseInt(hynesTicketsResult.rows[0]?.count || '0'), seubertTickets: parseInt(seubertTicketsResult.rows[0]?.count || '0'), universalTickets: parseInt(universalTicketsResult.rows[0]?.count || '0'), }; return NextResponse.json(stats); } catch (error) { console.error('Error fetching kiosk stats:', error); return NextResponse.json( { error: 'Failed to fetch kiosk stats' }, { status: 500 } ); } }