remove
This commit is contained in:
@@ -14,7 +14,6 @@ import { FoodModule } from './modules/foods/food.module';
|
|||||||
import { RolesModule } from './modules/roles/roles.module';
|
import { RolesModule } from './modules/roles/roles.module';
|
||||||
import { NotificationsModule } from './modules/notifications/notifications.module';
|
import { NotificationsModule } from './modules/notifications/notifications.module';
|
||||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||||
import { ContactModule } from './modules/contact/contact.module';
|
|
||||||
import { CacheModule } from '@nestjs/cache-manager';
|
import { CacheModule } from '@nestjs/cache-manager';
|
||||||
import { cacheConfig } from './config/cache.config';
|
import { cacheConfig } from './config/cache.config';
|
||||||
import { BusinessModule } from './modules/business/business.module';
|
import { BusinessModule } from './modules/business/business.module';
|
||||||
@@ -42,7 +41,6 @@ import { CatalogueModule } from './modules/catalogue/catalogue.module';
|
|||||||
RolesModule,
|
RolesModule,
|
||||||
NotificationsModule,
|
NotificationsModule,
|
||||||
EventEmitterModule.forRoot(),
|
EventEmitterModule.forRoot(),
|
||||||
ContactModule,
|
|
||||||
BusinessModule,
|
BusinessModule,
|
||||||
CatalogueModule,
|
CatalogueModule,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { ContactService } from './providers/contact.service';
|
|
||||||
import { ContactController } from './controllers/contact.controller';
|
|
||||||
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
|
||||||
import { Contact } from './entities/contact.entity';
|
|
||||||
import { ContactRepository } from './repositories/contact.repository';
|
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [MikroOrmModule.forFeature([Contact]), JwtModule],
|
|
||||||
controllers: [ContactController],
|
|
||||||
providers: [ContactService, ContactRepository],
|
|
||||||
exports: [ContactRepository],
|
|
||||||
})
|
|
||||||
export class ContactModule {}
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
import {
|
|
||||||
Controller,
|
|
||||||
Get,
|
|
||||||
Post,
|
|
||||||
Body,
|
|
||||||
Patch,
|
|
||||||
Param,
|
|
||||||
Query,
|
|
||||||
UseGuards,
|
|
||||||
Delete,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { ContactService } from '../providers/contact.service';
|
|
||||||
import { CreateContactDto } from '../dto/create-contact.dto';
|
|
||||||
import { FindContactsDto } from '../dto/find-contacts.dto';
|
|
||||||
import { UpdateContactStatusDto } from '../dto/update-contact-status.dto';
|
|
||||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam, ApiQuery, ApiHeader } from '@nestjs/swagger';
|
|
||||||
import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard';
|
|
||||||
import { SuperAdminAuthGuard } from 'src/modules/auth/guards/superAdminAuth.guard';
|
|
||||||
import { OptionalAuthGuard } from 'src/modules/auth/guards/optinalAuth.guard';
|
|
||||||
import { API_HEADER_SLUG } from 'src/common/constants';
|
|
||||||
import { Permission } from 'src/common/enums/permission.enum';
|
|
||||||
import { Permissions } from 'src/common/decorators/permissions.decorator';
|
|
||||||
import { RestId } from 'src/common/decorators/rest-id.decorator';
|
|
||||||
import { RestSlug } from 'src/common/decorators/rest-slug.decorator';
|
|
||||||
|
|
||||||
@ApiTags('contact')
|
|
||||||
@Controller()
|
|
||||||
export class ContactController {
|
|
||||||
constructor(private readonly contactService: ContactService) { }
|
|
||||||
|
|
||||||
@UseGuards(OptionalAuthGuard)
|
|
||||||
@Post('public/contact')
|
|
||||||
@ApiOperation({ summary: 'Create a new contact (public endpoint)' })
|
|
||||||
@ApiHeader(API_HEADER_SLUG)
|
|
||||||
async create(@Body() createContactDto: CreateContactDto, @RestSlug() slug: string, @RestId() restId?: string) {
|
|
||||||
return this.contactService.create(createContactDto, restId, slug);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@UseGuards(AdminAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@Permissions(Permission.MANAGE_CONTACTS)
|
|
||||||
@Get('admin/contact')
|
|
||||||
@ApiOperation({ summary: 'Get paginated list of contacts (admin only)' })
|
|
||||||
@ApiQuery({ name: 'page', required: false, type: Number })
|
|
||||||
@ApiQuery({ name: 'limit', required: false, type: Number })
|
|
||||||
@ApiQuery({ name: 'search', required: false, type: String })
|
|
||||||
@ApiQuery({ name: 'orderBy', required: false, type: String })
|
|
||||||
@ApiQuery({ name: 'order', required: false, enum: ['asc', 'desc'] })
|
|
||||||
@ApiQuery({ name: 'status', required: false, enum: ['new', 'seen'] })
|
|
||||||
async findAllPaginated(@RestId() restId: string, @Query() dto: FindContactsDto) {
|
|
||||||
return this.contactService.findAllPaginatedAdmin(dto, restId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@UseGuards(AdminAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@Permissions(Permission.MANAGE_CONTACTS)
|
|
||||||
@Get('admin/contact/:id')
|
|
||||||
@ApiOperation({ summary: 'Get contact details by ID (admin only)' })
|
|
||||||
@ApiParam({ name: 'id', description: 'Contact ID' })
|
|
||||||
async findOne(@RestId() restId: string, @Param('id') id: string) {
|
|
||||||
return this.contactService.findOne(id, restId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@UseGuards(AdminAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@Permissions(Permission.MANAGE_CONTACTS)
|
|
||||||
@Patch('admin/contact/:id/status')
|
|
||||||
@ApiOperation({ summary: 'Update contact status (admin only)' })
|
|
||||||
@ApiParam({ name: 'id', description: 'Contact ID' })
|
|
||||||
async updateStatus(@RestId() restId: string, @Param('id') id: string, @Body() dto: UpdateContactStatusDto) {
|
|
||||||
return this.contactService.updateStatus(id, dto, restId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@UseGuards(AdminAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@Permissions(Permission.MANAGE_CONTACTS)
|
|
||||||
@Delete('admin/contact/:id')
|
|
||||||
@ApiOperation({ summary: 'Hard delete a contact (admin only)' })
|
|
||||||
@ApiParam({ name: 'id', description: 'Contact ID' })
|
|
||||||
async remove(@RestId() restId: string, @Param('id') id: string) {
|
|
||||||
await this.contactService.remove(id, restId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*** Super Admin ***/
|
|
||||||
@UseGuards(SuperAdminAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@Get('super-admin/contact')
|
|
||||||
@ApiOperation({ summary: 'Get paginated list of contacts (admin only)' })
|
|
||||||
@ApiQuery({ name: 'page', required: false, type: Number })
|
|
||||||
@ApiQuery({ name: 'limit', required: false, type: Number })
|
|
||||||
@ApiQuery({ name: 'search', required: false, type: String })
|
|
||||||
@ApiQuery({ name: 'orderBy', required: false, type: String })
|
|
||||||
@ApiQuery({ name: 'order', required: false, enum: ['asc', 'desc'] })
|
|
||||||
@ApiQuery({ name: 'status', required: false, enum: ['new', 'seen'] })
|
|
||||||
async findAllPaginatedForSuper(@Query() dto: FindContactsDto) {
|
|
||||||
return this.contactService.findAllPaginatedSuper(dto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@UseGuards(SuperAdminAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@Get('super-admin/contact/:id')
|
|
||||||
@ApiOperation({ summary: 'Get contact details by ID (admin only)' })
|
|
||||||
@ApiParam({ name: 'id', description: 'Contact ID' })
|
|
||||||
async findOneForSuper(@Param('id') id: string) {
|
|
||||||
return this.contactService.findOne(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { IsNotEmpty, IsString, IsOptional, IsMobilePhone, IsEnum } from 'class-validator';
|
|
||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
||||||
import { ContactScope } from '../interface/interface';
|
|
||||||
|
|
||||||
export class CreateContactDto {
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
@ApiPropertyOptional({ example: 'Question about menu', description: 'Contact subject' })
|
|
||||||
subject?: string;
|
|
||||||
|
|
||||||
@IsNotEmpty()
|
|
||||||
@IsString()
|
|
||||||
@ApiProperty({ example: 'I have a question about your menu items', description: 'Contact content/message' })
|
|
||||||
content!: string;
|
|
||||||
|
|
||||||
@IsNotEmpty()
|
|
||||||
@IsEnum(['application', 'restaurant'])
|
|
||||||
@ApiProperty({ description: 'Contact Scope' })
|
|
||||||
scope!: ContactScope;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
@IsMobilePhone('fa-IR')
|
|
||||||
@ApiPropertyOptional({ example: '09362532122', description: 'Phone number' })
|
|
||||||
phone?: string;
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { IsOptional, IsNumber, IsString, IsIn, IsEnum } from 'class-validator';
|
|
||||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
|
||||||
import { Type } from 'class-transformer';
|
|
||||||
import { ContactStatusEnum } from '../interface/interface';
|
|
||||||
|
|
||||||
const sortOrderOptions = ['asc', 'desc'] as const;
|
|
||||||
type SortOrder = (typeof sortOrderOptions)[number];
|
|
||||||
|
|
||||||
export class FindContactsDto {
|
|
||||||
@IsOptional()
|
|
||||||
@IsNumber()
|
|
||||||
@Type(() => Number)
|
|
||||||
@ApiPropertyOptional({ example: 1, description: 'Page number', minimum: 1, default: 1 })
|
|
||||||
page?: number = 1;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsNumber()
|
|
||||||
@Type(() => Number)
|
|
||||||
@ApiPropertyOptional({ example: 10, description: 'Items per page', minimum: 1, default: 10 })
|
|
||||||
limit?: number = 10;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
@ApiPropertyOptional({ example: 'menu', description: 'Search in subject, content, or phone' })
|
|
||||||
search?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
@ApiPropertyOptional({ example: 'createdAt', description: 'Field to sort by', default: 'createdAt' })
|
|
||||||
orderBy?: string = 'createdAt';
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsIn(sortOrderOptions)
|
|
||||||
@ApiPropertyOptional({ example: 'desc', enum: sortOrderOptions, default: 'desc' })
|
|
||||||
order?: SortOrder = 'desc';
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsEnum(ContactStatusEnum)
|
|
||||||
@ApiPropertyOptional({ enum: ContactStatusEnum, description: 'Filter by status' })
|
|
||||||
status?: ContactStatusEnum;
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { IsEnum, IsNotEmpty } from 'class-validator';
|
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
|
||||||
import { ContactStatusEnum } from '../interface/interface';
|
|
||||||
|
|
||||||
export class UpdateContactStatusDto {
|
|
||||||
@IsNotEmpty()
|
|
||||||
@IsEnum(ContactStatusEnum)
|
|
||||||
@ApiProperty({ enum: ContactStatusEnum, description: 'New status for the contact' })
|
|
||||||
status!: ContactStatusEnum;
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import { PartialType } from '@nestjs/swagger';
|
|
||||||
import { CreateContactDto } from './create-contact.dto';
|
|
||||||
|
|
||||||
export class UpdateContactDto extends PartialType(CreateContactDto) {}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { Entity, Enum, ManyToOne, Property } from '@mikro-orm/core';
|
|
||||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
|
||||||
import { ContactScope, ContactStatusEnum } from '../interface/interface';
|
|
||||||
import { Restaurant } from '../../restaurants/entities/restaurant.entity';
|
|
||||||
|
|
||||||
@Entity({ tableName: 'contacts' })
|
|
||||||
export class Contact extends BaseEntity {
|
|
||||||
@Property({ nullable: true })
|
|
||||||
subject?: string | null = null;
|
|
||||||
|
|
||||||
@Property()
|
|
||||||
content!: string;
|
|
||||||
|
|
||||||
@Property({ nullable: true })
|
|
||||||
phone?: string | null = null;
|
|
||||||
|
|
||||||
@Enum(() => ContactScope)
|
|
||||||
scope: ContactScope
|
|
||||||
|
|
||||||
@ManyToOne(() => Restaurant, { nullable: true })
|
|
||||||
restaurant?: Restaurant | null = null;
|
|
||||||
|
|
||||||
|
|
||||||
@Property({ default: ContactStatusEnum.New })
|
|
||||||
status: ContactStatusEnum = ContactStatusEnum.New;
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
export enum ContactStatusEnum {
|
|
||||||
New = 'new',
|
|
||||||
Seen = 'seen',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ContactScope {
|
|
||||||
APPLICATION = 'application',
|
|
||||||
RESTAURANT = 'restaurant',
|
|
||||||
}
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
|
||||||
import { CreateContactDto } from '../dto/create-contact.dto';
|
|
||||||
import { FindContactsDto } from '../dto/find-contacts.dto';
|
|
||||||
import { UpdateContactStatusDto } from '../dto/update-contact-status.dto';
|
|
||||||
import { ContactRepository } from '../repositories/contact.repository';
|
|
||||||
import { Contact } from '../entities/contact.entity';
|
|
||||||
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
|
|
||||||
import { ContactStatusEnum } from '../interface/interface';
|
|
||||||
import { EntityManager } from '@mikro-orm/postgresql';
|
|
||||||
import { ContactScope } from '../interface/interface';
|
|
||||||
import { Restaurant } from '../../restaurants/entities/restaurant.entity';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ContactService {
|
|
||||||
constructor(
|
|
||||||
private readonly contactRepository: ContactRepository,
|
|
||||||
private readonly em: EntityManager,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
async create(dto: CreateContactDto, restId?: string, slug?: string): Promise<Contact> {
|
|
||||||
|
|
||||||
|
|
||||||
let restaurant: Restaurant | null = null;
|
|
||||||
|
|
||||||
if ((restId || slug) && dto.scope === ContactScope.RESTAURANT) {
|
|
||||||
restaurant = await this.em.findOne(Restaurant, {
|
|
||||||
...(restId ? { id: restId } : {})
|
|
||||||
, ...(slug ? { slug: slug } : {})
|
|
||||||
});
|
|
||||||
if (!restaurant) {
|
|
||||||
throw new NotFoundException(`Restaurant with ID ${restId} not found`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const contact = this.contactRepository.create({
|
|
||||||
...dto,
|
|
||||||
status: ContactStatusEnum.New,
|
|
||||||
restaurant,
|
|
||||||
});
|
|
||||||
await this.em.persistAndFlush(contact);
|
|
||||||
return contact;
|
|
||||||
}
|
|
||||||
|
|
||||||
async findAllPaginatedAdmin(dto: FindContactsDto, restaurantId?: string): Promise<PaginatedResult<Contact>> {
|
|
||||||
return this.contactRepository.findAllPaginated({
|
|
||||||
page: dto.page,
|
|
||||||
limit: dto.limit,
|
|
||||||
search: dto.search,
|
|
||||||
orderBy: dto.orderBy,
|
|
||||||
order: dto.order,
|
|
||||||
status: dto.status,
|
|
||||||
scope: ContactScope.RESTAURANT,
|
|
||||||
restaurantId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
async findAllPaginatedSuper(dto: FindContactsDto): Promise<PaginatedResult<Contact>> {
|
|
||||||
return this.contactRepository.findAllPaginated({
|
|
||||||
page: dto.page,
|
|
||||||
limit: dto.limit,
|
|
||||||
search: dto.search,
|
|
||||||
orderBy: dto.orderBy,
|
|
||||||
order: dto.order,
|
|
||||||
status: dto.status,
|
|
||||||
scope: ContactScope.APPLICATION
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async findOne(id: string, restaurantId?: string): Promise<Contact> {
|
|
||||||
const where: any = { id };
|
|
||||||
if (restaurantId) {
|
|
||||||
where.restaurant = restaurantId;
|
|
||||||
}
|
|
||||||
|
|
||||||
const contact = await this.contactRepository.findOne(where);
|
|
||||||
if (!contact) {
|
|
||||||
throw new NotFoundException(`Contact with ID ${id} not found`);
|
|
||||||
}
|
|
||||||
return contact;
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateStatus(id: string, dto: UpdateContactStatusDto, restaurantId?: string): Promise<Contact> {
|
|
||||||
const where: any = { id };
|
|
||||||
if (restaurantId) {
|
|
||||||
where.restaurant = restaurantId;
|
|
||||||
}
|
|
||||||
|
|
||||||
const contact = await this.contactRepository.findOne(where);
|
|
||||||
if (!contact) {
|
|
||||||
throw new NotFoundException(`Contact with ID ${id} not found`);
|
|
||||||
}
|
|
||||||
contact.status = dto.status;
|
|
||||||
await this.em.persistAndFlush(contact);
|
|
||||||
return contact;
|
|
||||||
}
|
|
||||||
|
|
||||||
async remove(id: string, restaurantId?: string): Promise<void> {
|
|
||||||
const where: any = { id };
|
|
||||||
if (restaurantId) {
|
|
||||||
where.restaurant = restaurantId;
|
|
||||||
}
|
|
||||||
|
|
||||||
const contact = await this.contactRepository.findOne(where);
|
|
||||||
if (!contact) {
|
|
||||||
throw new NotFoundException(`Contact with ID ${id} not found`);
|
|
||||||
}
|
|
||||||
await this.em.removeAndFlush(contact);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
|
|
||||||
import { FilterQuery } from '@mikro-orm/core';
|
|
||||||
import { Contact } from '../entities/contact.entity';
|
|
||||||
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
|
|
||||||
import { ContactScope, ContactStatusEnum } from '../interface/interface';
|
|
||||||
|
|
||||||
type FindContactsOpts = {
|
|
||||||
page?: number;
|
|
||||||
limit?: number;
|
|
||||||
search?: string;
|
|
||||||
orderBy?: string;
|
|
||||||
order?: 'asc' | 'desc';
|
|
||||||
status?: ContactStatusEnum;
|
|
||||||
scope?: ContactScope;
|
|
||||||
restaurantId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ContactRepository extends EntityRepository<Contact> {
|
|
||||||
constructor(readonly em: EntityManager) {
|
|
||||||
super(em, Contact);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find contacts with pagination and optional filters.
|
|
||||||
* Supports: search (subject/content/phone), status, ordering.
|
|
||||||
*/
|
|
||||||
async findAllPaginated(opts: FindContactsOpts = {}): Promise<PaginatedResult<Contact>> {
|
|
||||||
const { page = 1, limit = 10, search, scope, orderBy = 'createdAt', order = 'desc', status, restaurantId } = opts;
|
|
||||||
|
|
||||||
const offset = (page - 1) * limit;
|
|
||||||
|
|
||||||
const where: FilterQuery<Contact> = {};
|
|
||||||
|
|
||||||
if (status) {
|
|
||||||
where.status = status;
|
|
||||||
}
|
|
||||||
if (scope) {
|
|
||||||
where.scope = scope;
|
|
||||||
}
|
|
||||||
if (restaurantId) {
|
|
||||||
where.restaurant = restaurantId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (search) {
|
|
||||||
const pattern = `%${search}%`;
|
|
||||||
where.$or = [{ subject: { $ilike: pattern } }, { content: { $ilike: pattern } }, { phone: { $ilike: pattern } }];
|
|
||||||
}
|
|
||||||
|
|
||||||
const [data, total] = await this.findAndCount(where, {
|
|
||||||
limit,
|
|
||||||
offset,
|
|
||||||
orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const totalPages = Math.ceil(total / limit);
|
|
||||||
|
|
||||||
return {
|
|
||||||
data,
|
|
||||||
meta: {
|
|
||||||
total,
|
|
||||||
page,
|
|
||||||
limit,
|
|
||||||
totalPages,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user