notification module
This commit is contained in:
@@ -23,6 +23,7 @@ import { OrderModule } from './modules/order/order.module';
|
|||||||
import { LearningModule } from './modules/learnings/learning.module';
|
import { LearningModule } from './modules/learnings/learning.module';
|
||||||
import { CriticismModule } from './modules/criticisms/criticisms.module';
|
import { CriticismModule } from './modules/criticisms/criticisms.module';
|
||||||
import { AnnouncementModule } from './modules/announcements/announcement.module';
|
import { AnnouncementModule } from './modules/announcements/announcement.module';
|
||||||
|
import { TicketModule } from './modules/ticket/ticket.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -54,6 +55,7 @@ import { AnnouncementModule } from './modules/announcements/announcement.module'
|
|||||||
LearningModule,
|
LearningModule,
|
||||||
CriticismModule,
|
CriticismModule,
|
||||||
AnnouncementModule,
|
AnnouncementModule,
|
||||||
|
TicketModule,
|
||||||
],
|
],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [],
|
providers: [],
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export enum PermissionEnum {
|
|||||||
|
|
||||||
MANAGE_ANNOUNCEMENTS = 'manage_announcements',
|
MANAGE_ANNOUNCEMENTS = 'manage_announcements',
|
||||||
|
|
||||||
|
MANAGE_TICKETS = 'manage_tickets',
|
||||||
|
|
||||||
// Notif Permsissions
|
// Notif Permsissions
|
||||||
NEW_REQUEST_NOTIFICATION='new_request_notification',
|
NEW_REQUEST_NOTIFICATION='new_request_notification',
|
||||||
@@ -90,6 +91,7 @@ export const PermissionTitles: Record<PermissionEnum, string> = {
|
|||||||
[PermissionEnum.MANAGE_LEARNINGS]: "مدیریت آموزشها",
|
[PermissionEnum.MANAGE_LEARNINGS]: "مدیریت آموزشها",
|
||||||
[PermissionEnum.MANAGE_CRITICISMS]: "مدیریت انتقادها و پیشنهادها",
|
[PermissionEnum.MANAGE_CRITICISMS]: "مدیریت انتقادها و پیشنهادها",
|
||||||
[PermissionEnum.MANAGE_ANNOUNCEMENTS]: "مدیریت اطلاعیهها",
|
[PermissionEnum.MANAGE_ANNOUNCEMENTS]: "مدیریت اطلاعیهها",
|
||||||
|
[PermissionEnum.MANAGE_TICKETS]: "مدیریت تیکتها",
|
||||||
[PermissionEnum.NEW_REQUEST_NOTIFICATION]: "",
|
[PermissionEnum.NEW_REQUEST_NOTIFICATION]: "",
|
||||||
[PermissionEnum.NEW_PAYMENT_NOTIFICATION]: ""
|
[PermissionEnum.NEW_PAYMENT_NOTIFICATION]: ""
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,171 +1,124 @@
|
|||||||
import { Controller, Get, Body, Param, UseGuards, Query, Patch, Put, Post } from '@nestjs/common';
|
import { Controller, Get, Param, UseGuards, Query, Put } from '@nestjs/common';
|
||||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam, ApiQuery, ApiHeader } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam, ApiQuery } from '@nestjs/swagger';
|
||||||
import { NotificationService } from '../services/notification.service';
|
import { NotificationService } from '../services/notification.service';
|
||||||
import { AuthGuard } from '../../auth/guards/auth.guard';
|
import { AuthGuard } from '../../auth/guards/auth.guard';
|
||||||
import { UserId } from '../../../common/decorators/user-id.decorator';
|
import { UserId } from '../../../common/decorators/user-id.decorator';
|
||||||
import { AdminAuthGuard } from '../../auth/guards/adminAuth.guard';
|
import { AdminAuthGuard } from '../../auth/guards/adminAuth.guard';
|
||||||
import { AdminId } from 'src/common/decorators/admin-id.decorator';
|
import { AdminId } from 'src/common/decorators/admin-id.decorator';
|
||||||
import { NotificationMessage } from 'src/common/enums/message.enum';
|
import { NotificationMessage } from 'src/common/enums/message.enum';
|
||||||
import { Permissions } from 'src/common/decorators/permissions.decorator';
|
import { SmsLogRepository } from '../repositories/sms-log.repository';
|
||||||
import { PermissionEnum } from 'src/common/enums/permission.enum';
|
|
||||||
|
|
||||||
@ApiTags('notifications')
|
@ApiTags('notifications')
|
||||||
|
@ApiBearerAuth()
|
||||||
@Controller()
|
@Controller()
|
||||||
export class NotificationsController {
|
export class NotificationsController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly notificationService: NotificationService,
|
private readonly notificationService: NotificationService,
|
||||||
|
private readonly smsLogRepository: SmsLogRepository,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
// @UseGuards(AuthGuard)
|
@Get('public/notifications')
|
||||||
// @ApiBearerAuth()
|
@UseGuards(AuthGuard)
|
||||||
// @Get('public/notifications')
|
@ApiOperation({ summary: 'Get user notifications' })
|
||||||
// @ApiOperation({ summary: 'Get user restaurant notifications' })
|
@ApiQuery({ name: 'limit', required: false, type: Number, description: 'Number of items to return (default: 50)' })
|
||||||
|
@ApiQuery({
|
||||||
|
name: 'cursor',
|
||||||
|
required: false,
|
||||||
|
type: String,
|
||||||
|
description: 'Cursor for pagination (ID of the last item from previous page)',
|
||||||
|
})
|
||||||
|
@ApiQuery({ name: 'status', required: false, enum: ['seen', 'unseen'], description: 'Filter by notification status' })
|
||||||
|
async getUserNotifications(
|
||||||
|
@UserId() userId: string,
|
||||||
|
@Query('limit') limit?: number,
|
||||||
|
@Query('cursor') cursor?: string,
|
||||||
|
@Query('status') status?: 'seen' | 'unseen',
|
||||||
|
) {
|
||||||
|
return await this.notificationService.findUserNotifications(
|
||||||
|
userId,
|
||||||
|
limit ? parseInt(limit.toString(), 10) : 50,
|
||||||
|
cursor,
|
||||||
|
status,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// @ApiQuery({ name: 'limit', required: false, type: Number, description: 'Number of items to return (default: 50)' })
|
@UseGuards(AuthGuard)
|
||||||
// @ApiQuery({
|
@Get('public/notifications/unseen-count')
|
||||||
// name: 'cursor',
|
@ApiOperation({ summary: 'Get unseen notifications count for user' })
|
||||||
// required: false,
|
async getUserUnseenCount(@UserId() userId: string) {
|
||||||
// type: String,
|
const count = await this.notificationService.countUnseenByUser(userId);
|
||||||
// description: 'Cursor for pagination (ID of the last item from previous page)',
|
return { count };
|
||||||
// })
|
}
|
||||||
// @ApiQuery({ name: 'status', required: false, enum: ['seen', 'unseen'], description: 'Filter by notification status' })
|
|
||||||
// async getUserNotifications(
|
|
||||||
// @UserId() userId: string,
|
|
||||||
// @Query('limit') limit?: number,
|
|
||||||
// @Query('cursor') cursor?: string,
|
|
||||||
// @Query('status') status?: 'seen' | 'unseen',
|
|
||||||
// ) {
|
|
||||||
// return await this.notificationService.findByUserAndRestaurant(
|
|
||||||
// userId,
|
|
||||||
// limit ? parseInt(limit.toString(), 10) : 50,
|
|
||||||
// cursor,
|
|
||||||
// status,
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @UseGuards(AuthGuard)
|
@UseGuards(AuthGuard)
|
||||||
// @ApiBearerAuth()
|
@Put('public/notifications/:id')
|
||||||
// @Get('public/notifications/unseen-count')
|
@ApiOperation({ summary: 'Read a notification ' })
|
||||||
// @ApiOperation({ summary: 'Get unseen notifications count for user' })
|
@ApiParam({ name: 'id', description: 'Notification ID' })
|
||||||
|
async readNotificationUser(@Param('id') id: string, @UserId() userId: string) {
|
||||||
|
await this.notificationService.readNotificationAsUser(id, userId);
|
||||||
|
return { message: NotificationMessage.READ_SUCCESS };
|
||||||
|
}
|
||||||
|
|
||||||
// async getUserUnseenCount(@UserId() userId: string,) {
|
@UseGuards(AuthGuard)
|
||||||
// const count = await this.notificationService.countUnseenByUserAndRestaurant(userId,);
|
@Put('public/notifications/read/all')
|
||||||
// return { count };
|
@ApiOperation({ summary: 'Read all notification ' })
|
||||||
// }
|
async readAllNotificationUser(@UserId() userId: string) {
|
||||||
|
await this.notificationService.readAllNotifsAsUser(userId);
|
||||||
|
return { message: NotificationMessage.READ_SUCCESS };
|
||||||
|
}
|
||||||
|
/* ***************** Admin Endpoints ***************** */
|
||||||
|
|
||||||
// @UseGuards(AuthGuard)
|
@UseGuards(AdminAuthGuard)
|
||||||
// @ApiBearerAuth()
|
@Get('admin/notifications')
|
||||||
// @Put('public/notifications/:id')
|
@ApiOperation({ summary: 'Get Admin notifications' })
|
||||||
// @ApiOperation({ summary: 'Read a notification ' })
|
@ApiQuery({ name: 'limit', required: false, type: Number })
|
||||||
// @ApiParam({ name: 'id', description: 'Notification ID' })
|
@ApiQuery({
|
||||||
// async readNotificationUser(, @Param('id') id: string, @UserId() userId: string) {
|
name: 'cursor',
|
||||||
// await this.notificationService.readNotificationAsUser(id, userId,);
|
required: false,
|
||||||
// return { message: NotificationMessage.READ_SUCCESS };
|
type: String,
|
||||||
// }
|
description: 'Cursor for pagination (ID of the last item from previous page)',
|
||||||
|
})
|
||||||
|
@ApiQuery({ name: 'status', required: false, enum: ['seen', 'unseen'], description: 'Filter by notification status' })
|
||||||
|
async getAdminNotifications(
|
||||||
|
@AdminId() adminId: string,
|
||||||
|
@Query('limit') limit?: number,
|
||||||
|
@Query('cursor') cursor?: string,
|
||||||
|
@Query('status') status?: 'seen' | 'unseen',
|
||||||
|
) {
|
||||||
|
const parsedLimit = limit ? parseInt(limit.toString(), 10) : 50;
|
||||||
|
return await this.notificationService.findAdminNotifications(adminId, parsedLimit, cursor, status);
|
||||||
|
}
|
||||||
|
|
||||||
// @UseGuards(AuthGuard)
|
@UseGuards(AdminAuthGuard)
|
||||||
// @ApiBearerAuth()
|
@Get('admin/notifications/unseen-count')
|
||||||
// @Put('public/notifications/read/all')
|
@ApiOperation({ summary: 'Get unseen notifications count for admin' })
|
||||||
// @ApiOperation({ summary: 'Read all notification ' })
|
async getAdminUnseenCount(@AdminId() adminId: string) {
|
||||||
// async readAllNotificationUser(, @UserId() userId: string) {
|
const count = await this.notificationService.countUnseenByAdmin(adminId);
|
||||||
// await this.notificationService.readAllNotifsAsUser(userId,);
|
return { count };
|
||||||
// return { message: NotificationMessage.READ_SUCCESS };
|
}
|
||||||
// }
|
|
||||||
// /* ***************** Admin Endpoints ***************** */
|
|
||||||
|
|
||||||
// @UseGuards(AdminAuthGuard)
|
@UseGuards(AdminAuthGuard)
|
||||||
// @ApiBearerAuth()
|
@Put('admin/notifications/:id')
|
||||||
// @Get('admin/notifications')
|
@ApiOperation({ summary: 'Read a notification ' })
|
||||||
// @ApiOperation({ summary: 'Get Admin notifications' })
|
@ApiParam({ name: 'id', description: 'Notification ID' })
|
||||||
// @ApiQuery({ name: 'limit', required: false, type: Number })
|
async readNotificationAdmin(@AdminId() adminId: string, @Param('id') id: string) {
|
||||||
// @ApiQuery({
|
await this.notificationService.readNotificationAdmin(id, adminId);
|
||||||
// name: 'cursor',
|
return { message: NotificationMessage.READ_SUCCESS };
|
||||||
// required: false,
|
}
|
||||||
// type: String,
|
|
||||||
// description: 'Cursor for pagination (ID of the last item from previous page)',
|
|
||||||
// })
|
|
||||||
// @ApiQuery({ name: 'status', required: false, enum: ['seen', 'unseen'], description: 'Filter by notification status' })
|
|
||||||
// async getAdminNotifications(
|
|
||||||
// @AdminId() adminId: string,
|
|
||||||
// ,
|
|
||||||
// @Query('limit') limit?: number,
|
|
||||||
// @Query('cursor') cursor?: string,
|
|
||||||
// @Query('status') status?: 'seen' | 'unseen',
|
|
||||||
// ) {
|
|
||||||
// const parsedLimit = limit ? parseInt(limit.toString(), 10) : 50;
|
|
||||||
// return await this.notificationService.findByRestaurant( adminId, parsedLimit, cursor, status);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @UseGuards(AdminAuthGuard)
|
@UseGuards(AdminAuthGuard)
|
||||||
// @ApiBearerAuth()
|
@Put('admin/notifications/read/all')
|
||||||
// @Get('admin/notifications/unseen-count')
|
@ApiOperation({ summary: 'Read all notification ' })
|
||||||
// @ApiOperation({ summary: 'Get unseen notifications count for admin' })
|
async readAllNotificationAdmin(@AdminId() adminId: string) {
|
||||||
// async getAdminUnseenCount(@AdminId() adminId: string,) {
|
await this.notificationService.readAllNotifsAsAdmin(adminId);
|
||||||
// const count = await this.notificationService.countUnseenByRestaurant(adminId,);
|
return { message: NotificationMessage.READ_SUCCESS };
|
||||||
// return { count };
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
// @UseGuards(AdminAuthGuard)
|
|
||||||
// @ApiBearerAuth()
|
|
||||||
// @Put('admin/notifications/:id')
|
|
||||||
// @ApiOperation({ summary: 'Read a notification ' })
|
|
||||||
// @ApiParam({ name: 'id', description: 'Notification ID' })
|
|
||||||
// async readNotificationAdmin(, @AdminId() adminId: string, @Param('id') id: string) {
|
|
||||||
// await this.notificationService.readNotificationAdmin(id, adminId,);
|
|
||||||
// return { message: NotificationMessage.READ_SUCCESS };
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @UseGuards(AdminAuthGuard)
|
|
||||||
// @ApiBearerAuth()
|
|
||||||
// @Put('admin/notifications/read/all')
|
|
||||||
// @ApiOperation({ summary: 'Read all notification ' })
|
|
||||||
// async readAllNotificationAdmin(, @AdminId() adminId: string, @Param('id') id: string) {
|
|
||||||
// await this.notificationService.readAllNotifsAsAdmin(adminId,);
|
|
||||||
// return { message: NotificationMessage.READ_SUCCESS };
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @UseGuards(AdminAuthGuard)
|
|
||||||
// @ApiBearerAuth()
|
|
||||||
// @Permissions(Permission.MANAGE_SETTINGS)
|
|
||||||
// @Get('admin/notification-preferences')
|
|
||||||
// @ApiOperation({ summary: 'Get all notification preferences for a restaurant' })
|
|
||||||
// async getPreferences() {
|
|
||||||
// return this.preferenceService.findByRestaurant();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @UseGuards(AdminAuthGuard)
|
|
||||||
// @ApiBearerAuth()
|
|
||||||
// @Permissions(Permission.MANAGE_SETTINGS)
|
|
||||||
// @Patch('admin/notification-preferences/:id')
|
|
||||||
// @ApiOperation({ summary: 'Update notification channels' })
|
|
||||||
// @ApiParam({ name: 'id', description: 'Notification preference ID' })
|
|
||||||
// async updatePreference(
|
|
||||||
// ,
|
|
||||||
// @Param('id') preferenceId: string,
|
|
||||||
// @Body() dto: UpdatePreferenceDto,
|
|
||||||
// ) {
|
|
||||||
// return this.preferenceService.updatePreference(preferenceId, dto);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @UseGuards(AdminAuthGuard)
|
|
||||||
// @ApiBearerAuth()
|
|
||||||
// @Permissions(Permission.MANAGE_SETTINGS)
|
|
||||||
// @Post('admin/notification-preferences')
|
|
||||||
// @ApiOperation({ summary: 'Create notification preference' })
|
|
||||||
// async createPreference(
|
|
||||||
// ,
|
|
||||||
// @Body() dto: CreatePreferenceDto,
|
|
||||||
// ) {
|
|
||||||
// return this.preferenceService.create(dto);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @UseGuards(AdminAuthGuard)
|
|
||||||
// @ApiBearerAuth()
|
|
||||||
// @Get('admin/notifications/sms-usage')
|
|
||||||
// @ApiOperation({ summary: 'Get SMS usage for my restaurant' })
|
|
||||||
// async getRestaurantSmsUsage() {
|
|
||||||
// const smsCount = await this.notificationService.getSmsCountBy();
|
|
||||||
// return { smsCount };
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
@UseGuards(AdminAuthGuard)
|
||||||
|
@Get('admin/notifications/sms-usage')
|
||||||
|
@ApiOperation({ summary: 'Get SMS usage' })
|
||||||
|
async getRestaurantSmsUsage() {
|
||||||
|
const smsCount = await this.smsLogRepository.getTotalSmsCount();
|
||||||
|
return { smsCount };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ import { SmsListeners } from './listeners/sms.listeners';
|
|||||||
SmsLogRepository,
|
SmsLogRepository,
|
||||||
SmsListeners,
|
SmsListeners,
|
||||||
],
|
],
|
||||||
exports: [NotificationService, NotificationQueueService, PushNotificationService, SmsService],
|
exports: [NotificationService, NotificationQueueService,
|
||||||
|
PushNotificationService, SmsService, NotificationsGateway],
|
||||||
})
|
})
|
||||||
export class NotificationsModule { }
|
export class NotificationsModule { }
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { IsString, IsNotEmpty, IsOptional, IsArray, ValidateNested } from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import { IAttachment } from 'src/modules/order/interface/order.interface';
|
||||||
|
|
||||||
|
class AttachmentDto implements IAttachment {
|
||||||
|
@IsString()
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CreateTicketUserDto {
|
||||||
|
@ApiPropertyOptional({ description: 'Parent ticket ID for reply/thread' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
parentId?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Ticket subject' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
subject?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Ticket content', example: 'متن تیکت' })
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
content: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Attachments',
|
||||||
|
type: [AttachmentDto],
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => AttachmentDto)
|
||||||
|
attachments?: IAttachment[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import {
|
||||||
|
IsString,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsEnum,
|
||||||
|
IsArray,
|
||||||
|
ValidateNested,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import { TicketStatusEnum } from '../enum/status.enum';
|
||||||
|
import { IAttachment } from 'src/modules/order/interface/order.interface';
|
||||||
|
|
||||||
|
class AttachmentDto implements IAttachment {
|
||||||
|
@IsString()
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CreateTicketDto {
|
||||||
|
@ApiPropertyOptional({ description: 'Parent ticket ID for reply/thread' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
parentId?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Ticket subject' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
subject?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Ticket content', example: 'متن تیکت' })
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
content: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Ticket status',
|
||||||
|
enum: TicketStatusEnum,
|
||||||
|
default: TicketStatusEnum.OPEN,
|
||||||
|
})
|
||||||
|
@IsEnum(TicketStatusEnum)
|
||||||
|
status: TicketStatusEnum = TicketStatusEnum.OPEN;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Attachments',
|
||||||
|
type: [AttachmentDto],
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => AttachmentDto)
|
||||||
|
attachments?: IAttachment[];
|
||||||
|
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'User ID who created/owns the ticket' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
userId?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import {
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
IsNumber,
|
||||||
|
Min,
|
||||||
|
IsIn,
|
||||||
|
IsEnum,
|
||||||
|
IsDateString,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { Type, Transform } from 'class-transformer';
|
||||||
|
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { TicketStatusEnum } from '../enum/status.enum';
|
||||||
|
|
||||||
|
const sortOrderOptions = ['asc', 'desc'] as const;
|
||||||
|
type SortOrder = (typeof sortOrderOptions)[number];
|
||||||
|
|
||||||
|
export class SearchTicketQueryDto {
|
||||||
|
@ApiPropertyOptional({ default: 1, minimum: 1 })
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsNumber()
|
||||||
|
@Min(1)
|
||||||
|
page: number = 1;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ default: 10, minimum: 1 })
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsNumber()
|
||||||
|
@Min(1)
|
||||||
|
limit: number = 10;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Filter by ticket statuses',
|
||||||
|
enum: TicketStatusEnum,
|
||||||
|
isArray: true,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@Transform(({ value }: { value: unknown }) =>
|
||||||
|
Array.isArray(value) ? value : typeof value === 'string' ? value.split(',') : value,
|
||||||
|
)
|
||||||
|
@IsEnum(TicketStatusEnum, { each: true })
|
||||||
|
statuses?: TicketStatusEnum[];
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Filter by parent ticket (null for root tickets)' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
parentId?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Filter by admin ID' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
adminId?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Filter by user ID' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
userId?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Search in subject and content' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
search?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ format: 'date-time' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
from?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ format: 'date-time' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
to?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ default: 'createdAt' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
orderBy: string = 'createdAt';
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ enum: sortOrderOptions, default: 'desc' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(sortOrderOptions)
|
||||||
|
order: SortOrder = 'desc';
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { IsString, IsOptional, IsArray, ValidateNested } from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import { IAttachment } from 'src/modules/order/interface/order.interface';
|
||||||
|
|
||||||
|
class AttachmentDto implements IAttachment {
|
||||||
|
@IsString()
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdateTicketUserDto {
|
||||||
|
@ApiPropertyOptional({ description: 'Ticket subject' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
subject?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Ticket content' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
content?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Attachments',
|
||||||
|
type: [AttachmentDto],
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => AttachmentDto)
|
||||||
|
attachments?: IAttachment[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { PartialType } from '@nestjs/swagger';
|
||||||
|
import { CreateTicketDto } from './create-ticket.dto';
|
||||||
|
|
||||||
|
export class UpdateTicketDto extends PartialType(CreateTicketDto) {}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Collection, Entity, Enum, ManyToOne, OneToMany, OptionalProps, Property } from '@mikro-orm/core';
|
||||||
|
import { BaseEntity } from 'src/common/entities/base.entity';
|
||||||
|
import { TicketStatusEnum } from '../enum/status.enum';
|
||||||
|
import { IAttachment } from 'src/modules/order/interface/order.interface';
|
||||||
|
import { Admin } from 'src/modules/admin/entities/admin.entity';
|
||||||
|
import { User } from 'src/modules/user/entities/user.entity';
|
||||||
|
|
||||||
|
@Entity({ tableName: 'tickets' })
|
||||||
|
export class Ticket extends BaseEntity {
|
||||||
|
[OptionalProps]?: 'createdAt' | 'deletedAt'
|
||||||
|
|
||||||
|
@ManyToOne(() => Ticket, { nullable: true })
|
||||||
|
parent?: Ticket | null;
|
||||||
|
|
||||||
|
@OneToMany(() => Ticket, (ticket) => ticket.parent)
|
||||||
|
children = new Collection<Ticket>(this)
|
||||||
|
|
||||||
|
@Property({ nullable: true })
|
||||||
|
subject?: string | null;
|
||||||
|
|
||||||
|
@Property({ nullable: true })
|
||||||
|
content: string;
|
||||||
|
|
||||||
|
@Enum(() => TicketStatusEnum)
|
||||||
|
status: TicketStatusEnum;
|
||||||
|
|
||||||
|
@Property({ nullable: true })
|
||||||
|
attachments?: IAttachment[];
|
||||||
|
|
||||||
|
@ManyToOne(() => Admin, { nullable: true })
|
||||||
|
admin?: Admin | null;
|
||||||
|
|
||||||
|
@ManyToOne(() => User, { nullable: true })
|
||||||
|
user?: User | null;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export enum TicketStatusEnum {
|
||||||
|
OPEN = 'open',
|
||||||
|
CLOSED = 'closed',
|
||||||
|
PENDING = 'pending',
|
||||||
|
IN_PROGRESS = 'in_progress',
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
|
||||||
|
import { FilterQuery } from '@mikro-orm/core';
|
||||||
|
import { Ticket } from '../entities/ticket.entity';
|
||||||
|
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
|
||||||
|
import { SearchTicketQueryDto } from '../dto/search-ticket-query.dto';
|
||||||
|
import { TicketStatusEnum } from '../enum/status.enum';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TicketRepository extends EntityRepository<Ticket> {
|
||||||
|
constructor(readonly em: EntityManager) {
|
||||||
|
super(em, Ticket);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAllPaginated(dto: SearchTicketQueryDto): Promise<PaginatedResult<Ticket>> {
|
||||||
|
const {
|
||||||
|
page = 1,
|
||||||
|
limit = 10,
|
||||||
|
statuses,
|
||||||
|
parentId,
|
||||||
|
adminId,
|
||||||
|
userId,
|
||||||
|
search,
|
||||||
|
orderBy = 'createdAt',
|
||||||
|
order = 'desc',
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
} = dto;
|
||||||
|
|
||||||
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
|
const where: FilterQuery<Ticket> = {};
|
||||||
|
|
||||||
|
if (parentId !== undefined) {
|
||||||
|
if (parentId === null || parentId === '') {
|
||||||
|
where.parent = null;
|
||||||
|
} else {
|
||||||
|
where.parent = { id: parentId };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (adminId !== undefined) {
|
||||||
|
if (adminId === null || adminId === '') {
|
||||||
|
where.admin = null;
|
||||||
|
} else {
|
||||||
|
where.admin = { id: adminId };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userId !== undefined) {
|
||||||
|
if (userId === null || userId === '') {
|
||||||
|
where.user = null;
|
||||||
|
} else {
|
||||||
|
where.user = { id: userId };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statuses?.length) {
|
||||||
|
const normalizedStatuses = Array.isArray(statuses) ? statuses : [statuses];
|
||||||
|
const validStatuses = normalizedStatuses.filter((s): s is TicketStatusEnum => !!s);
|
||||||
|
if (validStatuses.length === 1) {
|
||||||
|
where.status = validStatuses[0];
|
||||||
|
} else if (validStatuses.length > 1) {
|
||||||
|
where.status = { $in: validStatuses };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (from || to) {
|
||||||
|
where.createdAt = {};
|
||||||
|
if (from) where.createdAt.$gte = new Date(from);
|
||||||
|
if (to) where.createdAt.$lte = new Date(to);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
const searchPattern = `%${search}%`;
|
||||||
|
where.$or = [
|
||||||
|
{ subject: { $ilike: searchPattern } },
|
||||||
|
{ content: { $ilike: searchPattern } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const [data, total] = await this.findAndCount(where, {
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' },
|
||||||
|
populate: ['parent', 'children', 'admin', 'user'],
|
||||||
|
filters: ['notDeleted'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(total / limit);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
meta: {
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
totalPages,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
Patch,
|
||||||
|
Param,
|
||||||
|
Delete,
|
||||||
|
Query,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { TicketService } from './ticket.service';
|
||||||
|
import { CreateTicketDto } from './dto/create-ticket.dto';
|
||||||
|
import { UpdateTicketDto } from './dto/update-ticket.dto';
|
||||||
|
import { CreateTicketUserDto } from './dto/create-ticket-user.dto';
|
||||||
|
import { UpdateTicketUserDto } from './dto/update-ticket-user.dto';
|
||||||
|
import { SearchTicketQueryDto } from './dto/search-ticket-query.dto';
|
||||||
|
import { AdminAuthGuard } from '../auth/guards/adminAuth.guard';
|
||||||
|
import { AuthGuard } from '../auth/guards/auth.guard';
|
||||||
|
import { Permissions } from 'src/common/decorators/permissions.decorator';
|
||||||
|
import { PermissionEnum } from 'src/common/enums/permission.enum';
|
||||||
|
import { UserId } from 'src/common/decorators/user-id.decorator';
|
||||||
|
import { AdminId } from 'src/common/decorators/admin-id.decorator';
|
||||||
|
|
||||||
|
@ApiTags('Ticket')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Controller()
|
||||||
|
export class TicketController {
|
||||||
|
constructor(private readonly ticketService: TicketService) { }
|
||||||
|
|
||||||
|
// ================== User routes ===================
|
||||||
|
|
||||||
|
@Post('public/tickets')
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
@ApiOperation({ summary: 'Create a ticket (as user)' })
|
||||||
|
createAsUser(@Body() createTicketUserDto: CreateTicketUserDto, @UserId() userId: string) {
|
||||||
|
return this.ticketService.createAsUser(createTicketUserDto, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('public/tickets')
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
@ApiOperation({ summary: 'Get my tickets with pagination' })
|
||||||
|
findMyTickets(@Query() queryDto: SearchTicketQueryDto, @UserId() userId: string) {
|
||||||
|
return this.ticketService.findMyTickets(userId, queryDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('public/tickets/:id')
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
@ApiOperation({ summary: 'Get one of my tickets by id' })
|
||||||
|
findOneAsUser(@Param('id') id: string, @UserId() userId: string) {
|
||||||
|
return this.ticketService.findOneAsUser(id, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch('public/tickets/:id')
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
@ApiOperation({ summary: 'Update a ticket (as user, own tickets only)' })
|
||||||
|
updateAsUser(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() updateTicketUserDto: UpdateTicketUserDto,
|
||||||
|
@UserId() userId: string,
|
||||||
|
) {
|
||||||
|
return this.ticketService.updateAsUser(id, updateTicketUserDto, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================== Admin routes ===================
|
||||||
|
|
||||||
|
@Post('admin/tickets')
|
||||||
|
@UseGuards(AdminAuthGuard)
|
||||||
|
@Permissions(PermissionEnum.MANAGE_TICKETS)
|
||||||
|
@ApiOperation({ summary: 'Create a ticket (as admin)' })
|
||||||
|
createAsAdmin(@Body() dto: CreateTicketDto, @AdminId() adminId: string) {
|
||||||
|
return this.ticketService.createAsAdmin(dto, adminId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('admin/tickets')
|
||||||
|
@UseGuards(AdminAuthGuard)
|
||||||
|
@Permissions(PermissionEnum.MANAGE_TICKETS)
|
||||||
|
@ApiOperation({ summary: 'Get all tickets with pagination' })
|
||||||
|
findAllAsAdmin(@Query() queryDto: SearchTicketQueryDto) {
|
||||||
|
return this.ticketService.findAll(queryDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('admin/tickets/:id')
|
||||||
|
@UseGuards(AdminAuthGuard)
|
||||||
|
@Permissions(PermissionEnum.MANAGE_TICKETS)
|
||||||
|
@ApiOperation({ summary: 'Get one ticket by id' })
|
||||||
|
findOneAsAdmin(@Param('id') id: string) {
|
||||||
|
return this.ticketService.findOne(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch('admin/tickets/:id')
|
||||||
|
@UseGuards(AdminAuthGuard)
|
||||||
|
@Permissions(PermissionEnum.MANAGE_TICKETS)
|
||||||
|
@ApiOperation({ summary: 'Update a ticket (as admin)' })
|
||||||
|
updateAsAdmin(@Param('id') id: string, @Body() updateTicketDto: UpdateTicketDto) {
|
||||||
|
return this.ticketService.update(id, updateTicketDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('admin/tickets/:id')
|
||||||
|
@UseGuards(AdminAuthGuard)
|
||||||
|
@Permissions(PermissionEnum.MANAGE_TICKETS)
|
||||||
|
@ApiOperation({ summary: 'Soft delete a ticket' })
|
||||||
|
remove(@Param('id') id: string) {
|
||||||
|
return this.ticketService.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||||
|
import { TicketService } from './ticket.service';
|
||||||
|
import { TicketController } from './ticket.controller';
|
||||||
|
import { Ticket } from './entities/ticket.entity';
|
||||||
|
import { TicketRepository } from './repositories/ticket.repository';
|
||||||
|
import { AuthModule } from '../auth/auth.module';
|
||||||
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [MikroOrmModule.forFeature([Ticket]), AuthModule,JwtModule],
|
||||||
|
controllers: [TicketController],
|
||||||
|
providers: [TicketService, TicketRepository],
|
||||||
|
exports: [TicketService],
|
||||||
|
})
|
||||||
|
export class TicketModule {}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common';
|
||||||
|
import { EntityManager } from '@mikro-orm/postgresql';
|
||||||
|
import { CreateTicketDto } from './dto/create-ticket.dto';
|
||||||
|
import { UpdateTicketDto } from './dto/update-ticket.dto';
|
||||||
|
import { CreateTicketUserDto } from './dto/create-ticket-user.dto';
|
||||||
|
import { UpdateTicketUserDto } from './dto/update-ticket-user.dto';
|
||||||
|
import { SearchTicketQueryDto } from './dto/search-ticket-query.dto';
|
||||||
|
import { Ticket } from './entities/ticket.entity';
|
||||||
|
import { TicketRepository } from './repositories/ticket.repository';
|
||||||
|
import { Admin } from 'src/modules/admin/entities/admin.entity';
|
||||||
|
import { User } from 'src/modules/user/entities/user.entity';
|
||||||
|
import { TicketStatusEnum } from './enum/status.enum';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TicketService {
|
||||||
|
constructor(
|
||||||
|
private readonly em: EntityManager,
|
||||||
|
private readonly ticketRepository: TicketRepository,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
async createAsAdmin(createTicketDto: CreateTicketDto, adminId: string) {
|
||||||
|
const { parentId, userId, ...rest } = createTicketDto;
|
||||||
|
|
||||||
|
let parent: Ticket | null = null;
|
||||||
|
if (parentId) {
|
||||||
|
parent = await this.ticketRepository.findOne({ id: parentId });
|
||||||
|
if (!parent) {
|
||||||
|
throw new NotFoundException('Parent ticket not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let admin: Admin | null = null;
|
||||||
|
if (adminId) {
|
||||||
|
admin = await this.em.findOne(Admin, { id: adminId });
|
||||||
|
if (!admin) {
|
||||||
|
throw new NotFoundException('Admin not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let user: User | null = null;
|
||||||
|
if (userId) {
|
||||||
|
user = await this.em.findOne(User, { id: userId });
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundException('User not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ticket = this.em.create(Ticket, {
|
||||||
|
...rest,
|
||||||
|
parent: parent ?? undefined,
|
||||||
|
admin: admin ?? undefined,
|
||||||
|
user: user ?? undefined,
|
||||||
|
});
|
||||||
|
await this.em.persistAndFlush(ticket);
|
||||||
|
return ticket;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createAsUser(createTicketUserDto: CreateTicketUserDto, userId: string) {
|
||||||
|
const user = await this.em.findOne(User, { id: userId });
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundException('User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { parentId, ...rest } = createTicketUserDto;
|
||||||
|
|
||||||
|
let parent: Ticket | null = null;
|
||||||
|
if (parentId) {
|
||||||
|
parent = await this.ticketRepository.findOne({ id: parentId }, { filters: ['notDeleted'] });
|
||||||
|
if (!parent) {
|
||||||
|
throw new NotFoundException('Parent ticket not found');
|
||||||
|
}
|
||||||
|
if (parent.user?.id !== userId) {
|
||||||
|
throw new ForbiddenException('You can only reply to your own tickets');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ticket = this.em.create(Ticket, {
|
||||||
|
...rest,
|
||||||
|
parent: parent ?? undefined,
|
||||||
|
user,
|
||||||
|
status: TicketStatusEnum.OPEN,
|
||||||
|
});
|
||||||
|
await this.em.persistAndFlush(ticket);
|
||||||
|
return ticket;
|
||||||
|
}
|
||||||
|
|
||||||
|
findAll(dto: SearchTicketQueryDto) {
|
||||||
|
return this.ticketRepository.findAllPaginated(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
findMyTickets(userId: string, dto: SearchTicketQueryDto) {
|
||||||
|
return this.ticketRepository.findAllPaginated({ ...dto, userId });
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOneAsUser(id: string, userId: string) {
|
||||||
|
const ticket = await this.ticketRepository.findOne(
|
||||||
|
{ id, user: { id: userId } },
|
||||||
|
{ populate: ['parent', 'children', 'admin', 'user'], filters: ['notDeleted'] },
|
||||||
|
);
|
||||||
|
if (!ticket) {
|
||||||
|
throw new NotFoundException('Ticket not found');
|
||||||
|
}
|
||||||
|
return ticket;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(id: string) {
|
||||||
|
const ticket = await this.ticketRepository.findOne(
|
||||||
|
{ id },
|
||||||
|
{ populate: ['parent', 'children', 'admin', 'user'], filters: ['notDeleted'] },
|
||||||
|
);
|
||||||
|
if (!ticket) {
|
||||||
|
throw new NotFoundException('Ticket not found');
|
||||||
|
}
|
||||||
|
return ticket;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, updateTicketDto: UpdateTicketDto) {
|
||||||
|
const ticket = await this.ticketRepository.findOne({ id }, { filters: ['notDeleted'] });
|
||||||
|
if (!ticket) {
|
||||||
|
throw new NotFoundException('Ticket not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { parentId, userId, ...rest } = updateTicketDto;
|
||||||
|
|
||||||
|
if (parentId !== undefined) {
|
||||||
|
if (parentId === null || parentId === '') {
|
||||||
|
ticket.parent = undefined;
|
||||||
|
} else {
|
||||||
|
const parent = await this.ticketRepository.findOne({ id: parentId });
|
||||||
|
if (!parent) {
|
||||||
|
throw new NotFoundException('Parent ticket not found');
|
||||||
|
}
|
||||||
|
ticket.parent = parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (userId !== undefined) {
|
||||||
|
if (userId === null || userId === '') {
|
||||||
|
ticket.user = undefined;
|
||||||
|
} else {
|
||||||
|
const user = await this.em.findOne(User, { id: userId });
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundException('User not found');
|
||||||
|
}
|
||||||
|
ticket.user = user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.em.assign(ticket, rest);
|
||||||
|
await this.em.flush();
|
||||||
|
return ticket;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateAsUser(id: string, updateTicketUserDto: UpdateTicketUserDto, userId: string) {
|
||||||
|
const ticket = await this.ticketRepository.findOne({ id }, { filters: ['notDeleted'] });
|
||||||
|
if (!ticket) {
|
||||||
|
throw new NotFoundException('Ticket not found');
|
||||||
|
}
|
||||||
|
if (ticket.user?.id !== userId) {
|
||||||
|
throw new ForbiddenException('You can only update your own tickets');
|
||||||
|
}
|
||||||
|
this.em.assign(ticket, updateTicketUserDto);
|
||||||
|
await this.em.flush();
|
||||||
|
return ticket;
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: string) {
|
||||||
|
const ticket = await this.ticketRepository.findOne({ id }, { filters: ['notDeleted'] });
|
||||||
|
if (!ticket) {
|
||||||
|
throw new NotFoundException('Ticket not found');
|
||||||
|
}
|
||||||
|
ticket.deletedAt = new Date();
|
||||||
|
await this.em.flush();
|
||||||
|
return ticket;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user