feat: ticketing system with technical/sales roles and department routing

Backend:
- Added TECHNICAL and SALES roles to UserRole enum
- Added TicketDepartment, TicketStatus, TicketPriority enums
- Created Ticket and TicketMessage entities with relationships
- Created TicketsModule with full CRUD service and controller
- Ticket routing: users create tickets to technical/sales departments
- Staff reply updates status (answered), user reply sets waiting
- Role-based access: technical staff sees technical tickets, sales sees sales tickets
- Admin sees all tickets with stats (total, open, avg response time)
- Updated access control: technical role has admin-level access (except role change)
- Sales role can view users and handle sales tickets
- Clusters controller: technical role can manage clusters/pools
- Users controller: technical/sales can view users, only admin changes roles

Frontend:
- New user ticket pages: list (with create form) + detail (chat-style messages)
- Staff ticket panel: filtered by department with status filters
- Admin all-tickets page with statistics dashboard and department/status filters
- Updated sidebar: role-based nav items (admin/technical/sales sections)
- Role badges in header for technical (blue) and sales (green)
- Admin users page: new roles in dropdowns, role change restricted to admin only
- Deploy page: technical role gets cluster selection access like admin
This commit is contained in:
keyhan
2026-04-06 12:36:41 +03:30
parent 4fd102468e
commit 3ab647be2e
20 changed files with 1301 additions and 56 deletions
@@ -0,0 +1,40 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Ticket } from './ticket.entity';
import { User } from '../../users/entities/user.entity';
import { UserRole } from '../../common/enums';
@Entity('ticket_messages')
export class TicketMessage {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'text' })
message: string;
@Column()
ticketId: string;
@ManyToOne(() => Ticket, (ticket) => ticket.messages, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'ticketId' })
ticket: Ticket;
@Column()
senderId: string;
@ManyToOne(() => User, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'senderId' })
sender: User;
@Column({ type: 'enum', enum: UserRole })
senderRole: UserRole;
@CreateDateColumn()
createdAt: Date;
}
@@ -0,0 +1,50 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
OneToMany,
JoinColumn,
} from 'typeorm';
import { TicketDepartment, TicketStatus, TicketPriority } from '../../common/enums';
import { User } from '../../users/entities/user.entity';
import { TicketMessage } from './ticket-message.entity';
@Entity('tickets')
export class Ticket {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
subject: string;
@Column({ type: 'enum', enum: TicketDepartment })
department: TicketDepartment;
@Column({ type: 'enum', enum: TicketStatus, default: TicketStatus.OPEN })
status: TicketStatus;
@Column({ type: 'enum', enum: TicketPriority, default: TicketPriority.MEDIUM })
priority: TicketPriority;
@Column()
userId: string;
@ManyToOne(() => User, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'userId' })
user: User;
@OneToMany(() => TicketMessage, (msg) => msg.ticket, { cascade: true })
messages: TicketMessage[];
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
@Column({ type: 'timestamp', nullable: true })
closedAt: Date;
}