feat: Add contact form submission and message management functionality.
This commit is contained in:
@@ -743,3 +743,20 @@ model UserReport {
|
|||||||
@@index([status])
|
@@index([status])
|
||||||
@@map("user_reports")
|
@@map("user_reports")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// CONTACT MESSAGES
|
||||||
|
// ============================================
|
||||||
|
model ContactMessage {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
name String
|
||||||
|
email String
|
||||||
|
phone String?
|
||||||
|
message String
|
||||||
|
isRead Boolean @default(false)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@index([isRead])
|
||||||
|
@@index([createdAt])
|
||||||
|
@@map("contact_messages")
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { NotificationsModule } from './notifications';
|
|||||||
import { TestimonialsModule } from './testimonials';
|
import { TestimonialsModule } from './testimonials';
|
||||||
import { SupportChatModule } from './support-chat';
|
import { SupportChatModule } from './support-chat';
|
||||||
import { UserReportsModule } from './user-reports';
|
import { UserReportsModule } from './user-reports';
|
||||||
|
import { ContactModule } from './contact';
|
||||||
import { StripeModule } from './stripe';
|
import { StripeModule } from './stripe';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
@@ -66,6 +67,7 @@ import { AppService } from './app.service';
|
|||||||
TestimonialsModule,
|
TestimonialsModule,
|
||||||
SupportChatModule,
|
SupportChatModule,
|
||||||
UserReportsModule,
|
UserReportsModule,
|
||||||
|
ContactModule,
|
||||||
StripeModule,
|
StripeModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
|
|||||||
74
src/contact/contact.controller.ts
Normal file
74
src/contact/contact.controller.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import {
|
||||||
|
Controller, Get, Post, Patch, Delete,
|
||||||
|
Body, Param, Query, UseGuards, HttpCode, HttpStatus,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation, ApiBearerAuth, ApiQuery } from '@nestjs/swagger';
|
||||||
|
import { ContactService } from './contact.service';
|
||||||
|
import { JwtAuthGuard } from '../auth/guards';
|
||||||
|
import { Public } from '../auth/decorators';
|
||||||
|
import { Roles } from '../auth/decorators/roles.decorator';
|
||||||
|
import { UserRole } from '@prisma/client';
|
||||||
|
import { IsString, IsEmail, IsOptional, IsNotEmpty } from 'class-validator';
|
||||||
|
|
||||||
|
class CreateContactMessageDto {
|
||||||
|
@IsString() @IsNotEmpty() name: string;
|
||||||
|
@IsEmail() email: string;
|
||||||
|
@IsString() @IsOptional() phone?: string;
|
||||||
|
@IsString() @IsNotEmpty() message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiTags('Contact')
|
||||||
|
@Controller('contact')
|
||||||
|
export class ContactController {
|
||||||
|
constructor(private readonly contactService: ContactService) {}
|
||||||
|
|
||||||
|
// Public - anyone can submit a contact form
|
||||||
|
@Post()
|
||||||
|
@Public()
|
||||||
|
@HttpCode(HttpStatus.CREATED)
|
||||||
|
@ApiOperation({ summary: 'Submit a contact form message' })
|
||||||
|
async createMessage(@Body() dto: CreateContactMessageDto) {
|
||||||
|
return this.contactService.createMessage(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin only
|
||||||
|
@Get('admin/all')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Roles(UserRole.ADMIN)
|
||||||
|
@ApiBearerAuth('JWT-auth')
|
||||||
|
@ApiOperation({ summary: 'Admin: Get all contact messages' })
|
||||||
|
@ApiQuery({ name: 'isRead', required: false, type: Boolean })
|
||||||
|
async getAllMessages(@Query('isRead') isRead?: string) {
|
||||||
|
const readFilter = isRead === 'true' ? true : isRead === 'false' ? false : undefined;
|
||||||
|
return this.contactService.getAllMessages(readFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('admin/counts')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Roles(UserRole.ADMIN)
|
||||||
|
@ApiBearerAuth('JWT-auth')
|
||||||
|
@ApiOperation({ summary: 'Admin: Get contact message counts' })
|
||||||
|
async getCounts() {
|
||||||
|
return this.contactService.getCounts();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch('admin/:id/read')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Roles(UserRole.ADMIN)
|
||||||
|
@ApiBearerAuth('JWT-auth')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiOperation({ summary: 'Admin: Mark a contact message as read' })
|
||||||
|
async markAsRead(@Param('id') id: string) {
|
||||||
|
return this.contactService.markAsRead(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('admin/:id')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Roles(UserRole.ADMIN)
|
||||||
|
@ApiBearerAuth('JWT-auth')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiOperation({ summary: 'Admin: Delete a contact message' })
|
||||||
|
async deleteMessage(@Param('id') id: string) {
|
||||||
|
return this.contactService.deleteMessage(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/contact/contact.module.ts
Normal file
12
src/contact/contact.module.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ContactController } from './contact.controller';
|
||||||
|
import { ContactService } from './contact.service';
|
||||||
|
import { PrismaModule } from '../prisma/prisma.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PrismaModule],
|
||||||
|
controllers: [ContactController],
|
||||||
|
providers: [ContactService],
|
||||||
|
exports: [ContactService],
|
||||||
|
})
|
||||||
|
export class ContactModule {}
|
||||||
42
src/contact/contact.service.ts
Normal file
42
src/contact/contact.service.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ContactService {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async createMessage(data: { name: string; email: string; phone?: string; message: string }) {
|
||||||
|
return this.prisma.contactMessage.create({ data });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllMessages(isRead?: boolean) {
|
||||||
|
const where = isRead !== undefined ? { isRead } : {};
|
||||||
|
return this.prisma.contactMessage.findMany({
|
||||||
|
where,
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async markAsRead(id: string) {
|
||||||
|
const msg = await this.prisma.contactMessage.findUnique({ where: { id } });
|
||||||
|
if (!msg) throw new NotFoundException('Contact message not found');
|
||||||
|
return this.prisma.contactMessage.update({
|
||||||
|
where: { id },
|
||||||
|
data: { isRead: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteMessage(id: string) {
|
||||||
|
const msg = await this.prisma.contactMessage.findUnique({ where: { id } });
|
||||||
|
if (!msg) throw new NotFoundException('Contact message not found');
|
||||||
|
return this.prisma.contactMessage.delete({ where: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCounts() {
|
||||||
|
const [total, unread] = await Promise.all([
|
||||||
|
this.prisma.contactMessage.count(),
|
||||||
|
this.prisma.contactMessage.count({ where: { isRead: false } }),
|
||||||
|
]);
|
||||||
|
return { total, unread };
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/contact/index.ts
Normal file
2
src/contact/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { ContactModule } from './contact.module';
|
||||||
|
export { ContactService } from './contact.service';
|
||||||
Reference in New Issue
Block a user