aboutsummaryrefslogtreecommitdiff
path: root/api/src/modules/tickets/tickets.controller.ts
blob: 552974adde96a4708f2268f084f3e9756260bd2c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
/**
 * tickets.controller.ts
 *
 * Lógica de negócio do módulo de tickets.
 * Orquestra o repository, aplica regras e formata respostas.
 * NÃO acessa o banco diretamente — delega ao repository.
 */

import type { FastifyReply, FastifyRequest } from 'fastify';
import { createTicket, countTickets, findManyTickets } from './tickets.repository.js';
import type { CreateTicketBody, ListTicketsQuery, TicketParams } from './tickets.schema.js';
import { tickets } from '@/db/schema.js';
import { eq } from 'drizzle-orm';

// ─── POST /tickets ────────────────────────────────────────────────────────────

export async function createTicketHandler(
  request: FastifyRequest<{ Body: CreateTicketBody }>,
  reply: FastifyReply,
) {
  const { title, description, priority, categoryId, creatorId } = request.body;

  const ticket = await createTicket(request.server.db, {
    title,
    description,
    priority,
    categoryId:  categoryId ?? null,
    assigneeId:  null,         // Chamados nascem sem agente atribuído
    creatorId,
    status:      'open',
    dueAt:       null,
    resolvedAt:  null,
  });

  request.server.events.emit('ticket.created', {
    id: ticket.id,
    title: ticket.title,
    creatorId: ticket.creatorId,
  });

  return reply.code(201).send(ticket);
}

// ─── GET /tickets ─────────────────────────────────────────────────────────────

export async function listTicketsHandler(
  request: FastifyRequest<{ Querystring: ListTicketsQuery }>,
  reply: FastifyReply,
) {
  const filters = request.query;
  const db = request.server.db;

  // Executa contagem e listagem em paralelo para não dobrar o tempo de resposta
  const [total, data] = await Promise.all([
    countTickets(db, filters),
    findManyTickets(db, filters),
  ]);

  const totalPages = Math.ceil(total / filters.limit);

  return reply.send({
    data,
    meta: {
      page:       filters.page,
      limit:      filters.limit,
      total,
      totalPages,
    },
  });
}

// ─── GET /tickets/:id ─────────────────────────────────────────────────────────

export async function getTicketByIdHandler(
  request: FastifyRequest<{ Params: TicketParams }>,
  reply: FastifyReply,
) {
  const { findTicketById } = await import('./tickets.repository.js');
  const ticket = await findTicketById(request.server.db, request.params.id);

  if (!ticket) {
    return reply.notFound(`Ticket #${request.params.id} não encontrado`);
  }

  return reply.send(ticket);
}

// ─── DELETE /tickets/:id ──────────────────────────────────────────────────────

export async function deleteTicketHandler(
  request: FastifyRequest<{ Params: TicketParams }>,
  reply: FastifyReply,
) {
  const db = request.server.db;
  const { id } = request.params;
  const callerUser = request.user!;

  const { findTicketById } = await import('./tickets.repository.js');
  const existingTicket = await findTicketById(db, id);

  if (!existingTicket) {
    return reply.notFound('Chamado não encontrado.');
  }

  if (callerUser.role !== 'admin' && existingTicket.creatorId !== callerUser.id) {
    return reply.forbidden('Você não tem permissão para excluir este chamado.');
  }

  await db.delete(tickets).where(eq(tickets.id, id));

  return reply.code(204).send();
}