wulf-pulse/app/api/veeam/ticket-analysis/route.ts
lorentz ea3471d38d feat: Veeam RPO analysis, comparison, ticket analysis + company teams table
- Add Veeam RPO analysis page (/veeam-analysis) and comparison page (/veeam-comparison)
- Add API routes: /api/veeam/rpo-analyze, rpo-comparison, rpo-offline-log, ticket-analysis
- Add veeam-rpo-service.ts enhancements (RPO logic, offline detection, comparison)
- Add veeam-analysis-state.ts and rmm-device-resolver.ts services
- Add migrations 065-068: company_teams, veeam_rpo_offline_log, rpo_comparison_tables, veeam_ticket_analysis
- Add backup-status page updates and nav links for new Veeam pages
- Add scripts: deactivate-cis-for-inactive-companies, workstation category updates
- Add docs: mimecast-api-guide, veeam-backup-alerting-recommendation, workstation-backup-overview, ticket-analyzer-prompt
- Minor: webhook-service, entity-sync, entity-mapper, sync-helpers, sync.ts, middleware.ts updates
2026-04-29 09:16:46 -04:00

133 lines
5.2 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import postgresClient from '@/lib/services/postgres-client';
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const page = Math.max(1, parseInt(searchParams.get('page') ?? '1'));
const limit = 50;
const offset = (page - 1) * limit;
const category = searchParams.get('category');
const company = searchParams.get('company');
// Build filter for ticket list only (aggregations always show all data)
const filterWhere: string[] = [];
const filterParams: any[] = [];
if (category) {
filterParams.push(category);
filterWhere.push(`problem_category = $${filterParams.length}`);
}
if (company) {
filterParams.push(`%${company}%`);
filterWhere.push(`company_name ILIKE $${filterParams.length}`);
}
const baseWhere = filterWhere.length
? `WHERE ticket_created_at >= DATE_TRUNC('year', NOW()) AND ${filterWhere.join(' AND ')}`
: `WHERE ticket_created_at >= DATE_TRUNC('year', NOW())`;
const [statsRes, categoryRes, resolutionRes, skillsRes, complexityRes, ticketsRes, totalRes, ytdRes] =
await Promise.all([
// Overall stats (unfiltered)
postgresClient.query(`
SELECT
COUNT(*) AS total_analyzed,
ROUND(AVG(hours_worked)::numeric, 2) AS avg_hours,
ROUND(100.0 * COUNT(*) FILTER (WHERE same_day_close)
/ NULLIF(COUNT(*), 0), 1) AS same_day_pct,
ROUND(100.0 * COUNT(*) FILTER (WHERE preventable = true)
/ NULLIF(COUNT(*) FILTER (WHERE preventable IS NOT NULL), 0), 1) AS preventable_pct,
ROUND(100.0 * COUNT(*) FILTER (WHERE device_was_offline = true)
/ NULLIF(COUNT(*) FILTER (WHERE device_was_offline IS NOT NULL), 0), 1) AS offline_pct,
ROUND(100.0 * COUNT(*) FILTER (WHERE backup_completed_before_tech = true)
/ NULLIF(COUNT(*) FILTER (WHERE backup_completed_before_tech IS NOT NULL), 0), 1) AS auto_resolved_pct
FROM veeam_ticket_analysis
WHERE ticket_created_at >= DATE_TRUNC('year', NOW())
`),
// By problem category (unfiltered)
postgresClient.query(`
SELECT problem_category,
COUNT(*) AS count,
ROUND(AVG(hours_worked)::numeric, 2) AS avg_hours,
ROUND(100.0 * COUNT(*) FILTER (WHERE same_day_close)
/ NULLIF(COUNT(*), 0), 1) AS same_day_pct
FROM veeam_ticket_analysis
WHERE ticket_created_at >= DATE_TRUNC('year', NOW())
GROUP BY problem_category
ORDER BY count DESC
`),
// By resolution type (unfiltered)
postgresClient.query(`
SELECT resolution_type, COUNT(*) AS count
FROM veeam_ticket_analysis
WHERE ticket_created_at >= DATE_TRUNC('year', NOW())
GROUP BY resolution_type
ORDER BY count DESC
`),
// Skills frequency (unfiltered)
postgresClient.query(`
SELECT skill, COUNT(*) AS count
FROM veeam_ticket_analysis, unnest(skills_required) AS skill
WHERE ticket_created_at >= DATE_TRUNC('year', NOW())
GROUP BY skill
ORDER BY count DESC
LIMIT 15
`),
// Complexity breakdown (unfiltered)
postgresClient.query(`
SELECT complexity, COUNT(*) AS count
FROM veeam_ticket_analysis
WHERE ticket_created_at >= DATE_TRUNC('year', NOW())
GROUP BY complexity
ORDER BY CASE complexity
WHEN 'trivial' THEN 1 WHEN 'low' THEN 2 WHEN 'medium' THEN 3 WHEN 'high' THEN 4 ELSE 5
END
`),
// Filtered ticket list
postgresClient.query(`
SELECT ticket_number, company_name, device_hostname,
ticket_created_at, ticket_closed_at, same_day_close,
hours_worked, problem_category, resolution_type, complexity,
device_was_offline, backup_completed_before_tech, preventable,
work_summary, recommended_procedure
FROM veeam_ticket_analysis
${baseWhere}
ORDER BY ticket_created_at DESC
LIMIT $${filterParams.length + 1} OFFSET $${filterParams.length + 2}
`, [...filterParams, limit, offset]),
// Filtered total
postgresClient.query(
`SELECT COUNT(*) FROM veeam_ticket_analysis ${baseWhere}`,
filterParams
),
// YTD total all backup tickets
postgresClient.query(`
SELECT COUNT(*) AS total FROM tickets
WHERE title ILIKE 'Veeam:%'
AND title NOT ILIKE '[Veeam RPO]%'
AND create_date >= DATE_TRUNC('year', NOW())
`),
]);
return NextResponse.json({
stats: {
...statsRes.rows[0],
total_ytd: parseInt(ytdRes.rows[0]?.total ?? 0),
},
by_category: categoryRes.rows,
by_resolution: resolutionRes.rows,
skills: skillsRes.rows,
by_complexity: complexityRes.rows,
tickets: ticketsRes.rows,
total: parseInt(totalRes.rows[0]?.count ?? 0),
page,
limit,
});
}