wulf-pulse/app/api/mobile/dashboard/route.ts
lorentz 89dbe6155b fix: mobile tickets/dashboard use classification filter instead of unpopulated MSP Service Model UDF
The hardcoded UDF filter (MSP Service Model = 'Wulf Managed') matched
only 1 company, making all mobile ticket views empty. Now reads
included_classifications from kiosk_settings (same as kiosk) which
correctly identifies all managed clients by classification ID.
2026-04-05 09:22:10 -04:00

102 lines
4.4 KiB
TypeScript

import { NextResponse } from 'next/server';
import { postgresClient } from '@/lib/services/postgres-client';
async function getManagedClassificationFilter(): Promise<string> {
const result = await postgresClient.query(
`SELECT setting_value FROM kiosk_settings WHERE setting_key = 'included_classifications'`
);
const value = result.rows[0]?.setting_value || '';
const ids = value ? value.split(',').map((s: string) => parseInt(s.trim(), 10)).filter((n: number) => !isNaN(n)) : [];
return ids.length > 0 ? `c.classification::integer IN (${ids.join(',')})` : 'true';
}
export async function GET() {
const classFilter = await getManagedClassificationFilter();
const [byStatus, byQueue, byPriority, recentActivity, sla] = await Promise.all([
postgresClient.query(`
SELECT t.status, COUNT(*) as count
FROM tickets t
INNER JOIN companies c ON c.id = t.company_id AND ${classFilter}
WHERE t.status != 5 AND t.is_deleted = false
GROUP BY t.status ORDER BY count DESC
`),
postgresClient.query(`
SELECT t.queue_id, q.label as queue_label, COUNT(*) as count
FROM tickets t
INNER JOIN companies c ON c.id = t.company_id AND ${classFilter}
LEFT JOIN queues q ON q.value = t.queue_id
WHERE t.status != 5 AND t.is_deleted = false
GROUP BY t.queue_id, q.label ORDER BY count DESC LIMIT 8
`),
postgresClient.query(`
SELECT t.priority, p.label, COUNT(*) as count
FROM tickets t
INNER JOIN companies c ON c.id = t.company_id AND ${classFilter}
LEFT JOIN priorities p ON p.value = t.priority
WHERE t.status != 5 AND t.is_deleted = false
GROUP BY t.priority, p.label ORDER BY count DESC
`),
postgresClient.query(`
SELECT t.id, t.ticket_number, t.title, t.status, t.priority,
t.last_activity_date, t.company_id,
c.company_name, q.label as queue_label,
r.first_name || ' ' || r.last_name as assigned_to
FROM tickets t
INNER JOIN companies c ON c.id = t.company_id AND ${classFilter}
LEFT JOIN queues q ON q.value = t.queue_id
LEFT JOIN resources r ON r.id = t.assigned_resource_id
WHERE t.status != 5 AND t.is_deleted = false AND t.last_activity_date IS NOT NULL
ORDER BY t.last_activity_date DESC LIMIT 10
`),
postgresClient.query(`
SELECT
COUNT(*) FILTER (WHERE t.first_response_date_time IS NOT NULL
AND EXTRACT(EPOCH FROM (t.first_response_date_time - t.create_date))/3600 <= 1) as resp_met,
COUNT(*) FILTER (WHERE t.first_response_date_time IS NOT NULL) as resp_total,
COUNT(*) FILTER (WHERE t.resolved_date_time IS NOT NULL
AND EXTRACT(EPOCH FROM (t.resolved_date_time - t.create_date))/3600 <= 24) as res_met,
COUNT(*) FILTER (WHERE t.resolved_date_time IS NOT NULL) as res_total
FROM tickets t
INNER JOIN companies c ON c.id = t.company_id AND ${classFilter}
WHERE t.create_date >= NOW() - INTERVAL '30 days' AND t.is_deleted = false
`),
]);
const statusLabels: Record<number, string> = {
1: 'New', 5: 'Complete', 7: 'In Progress', 8: 'In Progress',
9: 'Scheduled', 12: 'On Hold', 14: 'Waiting Customer',
19: 'Waiting Materials', 21: 'Dispatched', 25: 'In Review',
27: 'Pending Decision', 30: 'On Hold', 45: 'Escalated', 47: 'Waiting Customer',
56: 'Waiting Vendor', 58: 'Waiting Parts', 59: 'Pending Approval',
60: 'In Deployment', 66: 'Closed', 68: 'Resolved', 70: 'Customer Follow-Up', 71: 'Archived',
};
const slaRow = sla.rows[0];
return NextResponse.json({
open_total: byStatus.rows.reduce((s, r) => s + parseInt(r.count), 0),
by_status: byStatus.rows.map(r => ({
status: parseInt(r.status),
label: statusLabels[r.status] ?? `Status ${r.status}`,
count: parseInt(r.count),
})),
by_queue: byQueue.rows.map(r => ({
queue_id: r.queue_id,
label: r.queue_label ?? `Queue ${r.queue_id}`,
count: parseInt(r.count),
})),
by_priority: byPriority.rows.map(r => ({
priority: parseInt(r.priority),
label: r.label ?? `P${r.priority}`,
count: parseInt(r.count),
})),
recent: recentActivity.rows,
sla: {
response_met: parseInt(slaRow.resp_met ?? 0),
response_total: parseInt(slaRow.resp_total ?? 0),
resolution_met: parseInt(slaRow.res_met ?? 0),
resolution_total: parseInt(slaRow.res_total ?? 0),
},
});
}