From 8e9d623f248dcaa341427422539340443bf7b461 Mon Sep 17 00:00:00 2001 From: pradeepkumar Date: Fri, 20 Mar 2026 12:42:16 +0530 Subject: [PATCH] feat: Add contact form submission and message management functionality. --- prisma/schema.prisma | 17 +++++++ src/app.module.ts | 2 + src/contact/contact.controller.ts | 74 +++++++++++++++++++++++++++++++ src/contact/contact.module.ts | 12 +++++ src/contact/contact.service.ts | 42 ++++++++++++++++++ src/contact/index.ts | 2 + 6 files changed, 149 insertions(+) create mode 100644 src/contact/contact.controller.ts create mode 100644 src/contact/contact.module.ts create mode 100644 src/contact/contact.service.ts create mode 100644 src/contact/index.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f3e43f7..1303dcb 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -743,3 +743,20 @@ model UserReport { @@index([status]) @@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") +} diff --git a/src/app.module.ts b/src/app.module.ts index ed27d4c..7405927 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -21,6 +21,7 @@ import { NotificationsModule } from './notifications'; import { TestimonialsModule } from './testimonials'; import { SupportChatModule } from './support-chat'; import { UserReportsModule } from './user-reports'; +import { ContactModule } from './contact'; import { StripeModule } from './stripe'; import { AppController } from './app.controller'; import { AppService } from './app.service'; @@ -66,6 +67,7 @@ import { AppService } from './app.service'; TestimonialsModule, SupportChatModule, UserReportsModule, + ContactModule, StripeModule, ], controllers: [AppController], diff --git a/src/contact/contact.controller.ts b/src/contact/contact.controller.ts new file mode 100644 index 0000000..4408be8 --- /dev/null +++ b/src/contact/contact.controller.ts @@ -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); + } +} diff --git a/src/contact/contact.module.ts b/src/contact/contact.module.ts new file mode 100644 index 0000000..939534c --- /dev/null +++ b/src/contact/contact.module.ts @@ -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 {} diff --git a/src/contact/contact.service.ts b/src/contact/contact.service.ts new file mode 100644 index 0000000..a3e9cbc --- /dev/null +++ b/src/contact/contact.service.ts @@ -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 }; + } +} diff --git a/src/contact/index.ts b/src/contact/index.ts new file mode 100644 index 0000000..3e5559b --- /dev/null +++ b/src/contact/index.ts @@ -0,0 +1,2 @@ +export { ContactModule } from './contact.module'; +export { ContactService } from './contact.service';