58 lines
2.6 KiB
TypeScript
58 lines
2.6 KiB
TypeScript
|
|
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<Ticket[]> {
|
||
|
|
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<Ticket | null> {
|
||
|
|
const { rows } = await pool.query(`SELECT * FROM tickets WHERE number = $1`, [number]);
|
||
|
|
return rows[0] ? rowToTicket(rows[0]) : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function getStats(): Promise<StatsResponse> {
|
||
|
|
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) })),
|
||
|
|
};
|
||
|
|
}
|