import { pool, rowToTicket } from './db'; import type { Ticket, TicketFilters, StatsResponse } from './types'; // List tickets with optional filters. Text search spans number + short desc + // assignee. Ordered newest-activity-first so the operational board is useful. export async function listTickets(f: TicketFilters): Promise { const where: string[] = []; const params: unknown[] = []; const add = (sql: string, val: unknown) => { params.push(val); where.push(sql.replace('$?', `$${params.length}`)); }; if (f.status) add('status = $?', f.status); if (f.state) add('state = $?', f.state); if (f.group) add('assignment_group = $?', f.group); if (f.assignee) add('assigned_to = $?', f.assignee); if (f.q) { params.push(`%${f.q.toLowerCase()}%`); const p = `$${params.length}`; where.push(`(lower(number) LIKE ${p} OR lower(short_desc) LIKE ${p} OR lower(coalesce(assigned_to,'')) LIKE ${p})`); } const sql = ` SELECT * FROM tickets ${where.length ? 'WHERE ' + where.join(' AND ') : ''} ORDER BY last_activity_at DESC NULLS LAST, number DESC LIMIT 2000 `; const { rows } = await pool.query(sql, params); return rows.map(rowToTicket); } export async function getTicket(number: string): Promise { const { rows } = await pool.query(`SELECT * FROM tickets WHERE number = $1`, [number]); return rows[0] ? rowToTicket(rows[0]) : null; } export async function getStats(): Promise { const [totals, byState, byGroup, byAssignee] = await Promise.all([ pool.query<{ status: string; n: string }>(`SELECT status, count(*)::text AS n FROM tickets GROUP BY status`), pool.query<{ state: string; n: string }>( `SELECT state, count(*)::text AS n FROM tickets WHERE status='active' GROUP BY state ORDER BY count(*) DESC`), pool.query<{ g: string; n: string }>( `SELECT coalesce(assignment_group,'—') AS g, count(*)::text AS n FROM tickets WHERE status='active' GROUP BY g ORDER BY count(*) DESC`), pool.query<{ a: string; n: string }>( `SELECT coalesce(assigned_to,'—') AS a, count(*)::text AS n FROM tickets WHERE status='active' GROUP BY a ORDER BY count(*) DESC LIMIT 20`), ]); const active = Number(totals.rows.find(r => r.status === 'active')?.n ?? '0'); const closed = Number(totals.rows.find(r => r.status === 'closed')?.n ?? '0'); return { total: active + closed, active, closed, byState: byState.rows.map(r => ({ state: r.state || '—', count: Number(r.n) })), byGroup: byGroup.rows.map(r => ({ group: r.g, count: Number(r.n) })), byAssignee: byAssignee.rows.map(r => ({ assignee: r.a, count: Number(r.n) })), }; }