- Created kiosk_settings table for configuration storage - Added API endpoints for kiosk settings (GET/POST) - Filter out co-managed clients (configurable company exclusions) - Built comprehensive settings UI at /kiosk/settings - Allow excluding specific companies from kiosk display - Configurable cycle interval, refresh interval, and RMM alert toggle - Updated dashboard link to point to settings page - Applied company exclusion filter to all ticket queries and activity feed - Default excludes Thrasher Group (ID: 29861361)
262 lines
9.2 KiB
TypeScript
262 lines
9.2 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { postgresClient } from '@/lib/services/postgres-client';
|
|
|
|
async function getExcludedCompanyIds(): Promise<number[]> {
|
|
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 [];
|
|
}
|
|
}
|
|
|
|
export async function GET(request: NextRequest) {
|
|
try {
|
|
// Get excluded company IDs (co-managed clients)
|
|
const excludedCompanyIds = await getExcludedCompanyIds();
|
|
const excludeCompanyFilter = excludedCompanyIds.length > 0
|
|
? `AND company_id NOT IN (${excludedCompanyIds.join(',')})`
|
|
: '';
|
|
|
|
// Critical tickets (Priority 1-3) - exclude RMM alerts (source = 8) and co-managed clients
|
|
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 }
|
|
);
|
|
}
|
|
}
|