feat: unanswered ticket badges, dashboard counts, cross-department ticket creation

- Add /tickets/unanswered-counts API endpoint for staff/admin
- Show red badge with unanswered count on sidebar nav (staff tickets, admin tickets)
- Add summary cards (unanswered/answered/total) to staff tickets page
- Highlight unanswered count in admin tickets stats cards
- Staff can create tickets to OTHER departments only (not their own)
- Frontend hides own department from ticket creation dropdown
- Sidebar badges auto-refresh every 30 seconds
This commit is contained in:
keyhan
2026-04-06 13:55:15 +03:30
parent a1dbd0a85f
commit a64f8407b9
6 changed files with 123 additions and 23 deletions
+16 -1
View File
@@ -8,6 +8,7 @@ import {
Query,
UseGuards,
Request,
ForbiddenException,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { ApiTags, ApiOperation, ApiBearerAuth, ApiQuery } from '@nestjs/swagger';
@@ -29,8 +30,15 @@ export class TicketsController {
// ═══════════════════════════════════════════
@Post()
@ApiOperation({ summary: 'Create a new ticket (any user)' })
@ApiOperation({ summary: 'Create a new ticket' })
async create(@Request() req: any, @Body() dto: CreateTicketDto) {
// Staff cannot create tickets to their own department
if (req.user.role === UserRole.TECHNICAL && dto.department === 'technical') {
throw new ForbiddenException('You cannot create a ticket to your own department');
}
if (req.user.role === UserRole.SALES && dto.department === 'sales') {
throw new ForbiddenException('You cannot create a ticket to your own department');
}
return this.ticketsService.create(req.user.id, req.user.role, dto);
}
@@ -77,6 +85,13 @@ export class TicketsController {
return this.ticketsService.getStats();
}
@Get('unanswered-counts')
@Roles(UserRole.ADMIN, UserRole.TECHNICAL, UserRole.SALES)
@ApiOperation({ summary: 'Get unanswered ticket counts per department (Staff/Admin)' })
async getUnansweredCounts() {
return this.ticketsService.getUnansweredCounts();
}
// ═══════════════════════════════════════════
// PARAMETERIZED ROUTES (after static routes)
// ═══════════════════════════════════════════
+13
View File
@@ -119,6 +119,19 @@ export class TicketsService {
await this.ticketsRepo.update(ticketId, data);
}
// ─── Unanswered ticket counts ───
async getUnansweredCounts(): Promise<{ technical: number; sales: number; total: number }> {
const unanswered = await this.ticketsRepo.find({
where: [
{ status: TicketStatus.OPEN },
{ status: TicketStatus.WAITING },
],
});
const technical = unanswered.filter((t) => t.department === TicketDepartment.TECHNICAL).length;
const sales = unanswered.filter((t) => t.department === TicketDepartment.SALES).length;
return { technical, sales, total: technical + sales };
}
// ─── Staff: List department tickets ───
async findByDepartment(department: TicketDepartment, status?: TicketStatus): Promise<Ticket[]> {
const where: any = { department };