- Add /admin/display-settings page with Kiosk and Mobile sections - Company category checkbox filter + excluded companies searchable multi-select - New DB tables: company_categories, company_types (migration 064) - Sync COMPANY_CATEGORIES via CompanyCategories entity (id/name/isActive) - Sync COMPANY_TYPES via Companies.companyType picklist - Add to EntityType, ENTITY_DEPENDENCIES, sync-helpers, entity-mapper, entity-sync - New API routes: /api/admin/display-settings (GET/POST), /api/data/company-categories, /api/data/companies-list - Update all 4 routes (kiosk/stats, kiosk/activity, mobile/tickets, mobile/dashboard) to filter by kiosk_settings company_category_ids + excluded_company_ids - Add Display Settings nav link (SlidersHorizontal icon) to Admin menu - Seed kiosk_settings: kiosk_company_category_ids=1, mobile_company_category_ids=1
115 lines
5 KiB
TypeScript
115 lines
5 KiB
TypeScript
import { NextResponse } from 'next/server';
|
|
import { postgresClient } from '@/lib/services/postgres-client';
|
|
|
|
async function getMobileClassFilter(): Promise<string> {
|
|
try {
|
|
const result = await postgresClient.query(
|
|
`SELECT setting_key, setting_value FROM kiosk_settings WHERE setting_key IN ('mobile_company_category_ids', 'mobile_excluded_company_ids')`
|
|
);
|
|
const map: Record<string, string> = {};
|
|
result.rows.forEach((r: any) => { map[r.setting_key] = r.setting_value || ''; });
|
|
|
|
const catIds = (map['mobile_company_category_ids'] || '1')
|
|
.split(',').map((s: string) => parseInt(s.trim(), 10)).filter((n: number) => !isNaN(n));
|
|
const exclIds = (map['mobile_excluded_company_ids'] || '')
|
|
.split(',').map((s: string) => parseInt(s.trim(), 10)).filter((n: number) => !isNaN(n));
|
|
|
|
const catCond = catIds.length > 0 ? `c.company_category_id IN (${catIds.join(',')})` : 'true';
|
|
const exclCond = exclIds.length > 0 ? `c.id NOT IN (${exclIds.join(',')})` : '';
|
|
return [catCond, exclCond].filter(Boolean).join(' AND ');
|
|
} catch (error) {
|
|
console.error('Error fetching mobile company filter:', error);
|
|
return 'c.company_category_id = 1';
|
|
}
|
|
}
|
|
|
|
export async function GET() {
|
|
const classFilter = await getMobileClassFilter();
|
|
|
|
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),
|
|
},
|
|
});
|
|
}
|