- 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
133 lines
5.2 KiB
TypeScript
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,
|
|
});
|
|
}
|