Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import { AccessControlModule } from './access-control/access-control.module';
import { WaitlistModule } from './waitlist/waitlist.module';
import { EventsModule } from './events/events.module';
import { MembershipPlansModule } from './membership-plans/membership-plans.module';
import { MaintenanceModule } from './maintenance/maintenance.module';
import { LeadsModule } from './leads/leads.module';

@Module({
imports: [
Expand Down Expand Up @@ -114,6 +116,8 @@ import { MembershipPlansModule } from './membership-plans/membership-plans.modul
WaitlistModule,
EventsModule,
MembershipPlansModule,
MaintenanceModule,
LeadsModule,
],
controllers: [AppController],
providers: [
Expand Down
22 changes: 22 additions & 0 deletions backend/src/leads/dto/create-lead.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { IsEmail, IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { LeadSource } from '../enums/lead-source.enum';

export class CreateLeadDto {
@IsString()
@IsNotEmpty()
name: string;

@IsEmail()
email: string;

@IsOptional()
@IsString()
phone?: string;

@IsOptional()
@IsString()
company?: string;

@IsEnum(LeadSource)
source: LeadSource;
}
30 changes: 30 additions & 0 deletions backend/src/leads/dto/lead-query.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { IsEnum, IsInt, IsOptional, IsUUID, Min } from 'class-validator';
import { Type } from 'class-transformer';
import { LeadStatus } from '../enums/lead-status.enum';
import { LeadSource } from '../enums/lead-source.enum';

export class LeadQueryDto {
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
page?: number = 1;

@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
limit?: number = 20;

@IsOptional()
@IsEnum(LeadStatus)
status?: LeadStatus;

@IsOptional()
@IsEnum(LeadSource)
source?: LeadSource;

@IsOptional()
@IsUUID()
assignedToStaffId?: string;
}
16 changes: 16 additions & 0 deletions backend/src/leads/dto/update-lead.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { IsEnum, IsOptional, IsString, IsUUID } from 'class-validator';
import { LeadStatus } from '../enums/lead-status.enum';

export class UpdateLeadDto {
@IsOptional()
@IsEnum(LeadStatus)
status?: LeadStatus;

@IsOptional()
@IsString()
notes?: string;

@IsOptional()
@IsUUID()
assignedToStaffId?: string;
}
59 changes: 59 additions & 0 deletions backend/src/leads/entities/lead.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
JoinColumn,
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn,
} from 'typeorm';
import { User } from '../../users/entities/user.entity';
import { LeadSource } from '../enums/lead-source.enum';
import { LeadStatus } from '../enums/lead-status.enum';

@Entity('leads')
export class Lead {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column()
name: string;

@Column()
email: string;

@Column({ nullable: true })
phone: string | null;

@Column({ nullable: true })
company: string | null;

@Column({ type: 'enum', enum: LeadSource, default: LeadSource.OTHER })
source: LeadSource;

@Column({ type: 'enum', enum: LeadStatus, default: LeadStatus.NEW })
status: LeadStatus;

@Column({ type: 'text', nullable: true })
notes: string | null;

@Column('uuid', { nullable: true })
assignedToStaffId: string | null;

@ManyToOne(() => User, { nullable: true, onDelete: 'SET NULL' })
@JoinColumn({ name: 'assignedToStaffId' })
assignedToStaff: User;

@Column({ type: 'timestamptz', nullable: true })
convertedAt: Date | null;

@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn()
updatedAt: Date;

@DeleteDateColumn()
deletedAt: Date | null;
}
6 changes: 6 additions & 0 deletions backend/src/leads/enums/lead-source.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export enum LeadSource {
CONTACT_FORM = 'CONTACT_FORM',
REFERRAL = 'REFERRAL',
WALK_IN = 'WALK_IN',
OTHER = 'OTHER',
}
7 changes: 7 additions & 0 deletions backend/src/leads/enums/lead-status.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export enum LeadStatus {
NEW = 'NEW',
CONTACTED = 'CONTACTED',
QUALIFIED = 'QUALIFIED',
CONVERTED = 'CONVERTED',
LOST = 'LOST',
}
69 changes: 69 additions & 0 deletions backend/src/leads/leads.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {
Controller,
Get,
Post,
Patch,
Delete,
Param,
Body,
Query,
UseGuards,
ParseUUIDPipe,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { LeadsService } from './leads.service';
import { CreateLeadDto } from './dto/create-lead.dto';
import { UpdateLeadDto } from './dto/update-lead.dto';
import { LeadQueryDto } from './dto/lead-query.dto';
import { JwtAuthGuard } from '../auth/guard/jwt.auth.guard';
import { RolesGuard } from '../auth/guard/roles.guard';
import { Roles } from '../auth/decorators/roles.decorators';
import { UserRole } from '../users/enums/userRoles.enum';

@ApiTags('Leads')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.ADMIN, UserRole.STAFF)
@Controller('leads')
export class LeadsController {
constructor(private readonly service: LeadsService) {}

@Post()
async create(@Body() dto: CreateLeadDto) {
const data = await this.service.create(dto);
return { message: 'Lead created', data };
}

@Get()
async findAll(@Query() query: LeadQueryDto) {
return this.service.findAll(query);
}

@Get(':id')
async findOne(@Param('id', ParseUUIDPipe) id: string) {
const data = await this.service.findOne(id);
return { data };
}

@Patch(':id')
async update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateLeadDto) {
const data = await this.service.update(id, dto);
return { message: 'Lead updated', data };
}

@Post(':id/convert')
async convert(@Param('id', ParseUUIDPipe) id: string) {
const data = await this.service.convert(id);
return { message: 'Lead converted', data };
}

@Delete(':id')
@Roles(UserRole.ADMIN)
@HttpCode(HttpStatus.OK)
async remove(@Param('id', ParseUUIDPipe) id: string) {
await this.service.softDelete(id);
return { message: 'Lead deleted' };
}
}
13 changes: 13 additions & 0 deletions backend/src/leads/leads.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Lead } from './entities/lead.entity';
import { LeadsService } from './leads.service';
import { LeadsController } from './leads.controller';

@Module({
imports: [TypeOrmModule.forFeature([Lead])],
controllers: [LeadsController],
providers: [LeadsService],
exports: [LeadsService],
})
export class LeadsModule {}
71 changes: 71 additions & 0 deletions backend/src/leads/leads.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Lead } from './entities/lead.entity';
import { CreateLeadDto } from './dto/create-lead.dto';
import { UpdateLeadDto } from './dto/update-lead.dto';
import { LeadQueryDto } from './dto/lead-query.dto';
import { LeadStatus } from './enums/lead-status.enum';
import { LeadSource } from './enums/lead-source.enum';

@Injectable()
export class LeadsService {
constructor(
@InjectRepository(Lead)
private readonly repo: Repository<Lead>,
) {}

async create(dto: CreateLeadDto): Promise<Lead> {
return this.repo.save(this.repo.create(dto));
}

async createFromContactForm(name: string, email: string, phone?: string): Promise<Lead> {
return this.repo.save(this.repo.create({
name, email, phone: phone ?? null,
source: LeadSource.CONTACT_FORM,
status: LeadStatus.NEW,
}));
}

async findAll(query: LeadQueryDto) {
const { page = 1, limit = 20, status, source, assignedToStaffId } = query;
const where: Record<string, unknown> = {};
if (status) where.status = status;
if (source) where.source = source;
if (assignedToStaffId) where.assignedToStaffId = assignedToStaffId;

const [items, total] = await this.repo.findAndCount({
where,
order: { createdAt: 'DESC' },
skip: (page - 1) * limit,
take: limit,
relations: ['assignedToStaff'],
withDeleted: false,
});
return { items, total, page, limit, totalPages: Math.ceil(total / limit) };
}

async findOne(id: string): Promise<Lead> {
const item = await this.repo.findOne({ where: { id }, relations: ['assignedToStaff'] });
if (!item) throw new NotFoundException(`Lead ${id} not found`);
return item;
}

async update(id: string, dto: UpdateLeadDto): Promise<Lead> {
const item = await this.findOne(id);
Object.assign(item, dto);
return this.repo.save(item);
}

async convert(id: string): Promise<Lead> {
const item = await this.findOne(id);
item.status = LeadStatus.CONVERTED;
item.convertedAt = new Date();
return this.repo.save(item);
}

async softDelete(id: string): Promise<void> {
const item = await this.findOne(id);
await this.repo.softDelete(item.id);
}
}
19 changes: 19 additions & 0 deletions backend/src/maintenance/dto/create-maintenance-request.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { IsEnum, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
import { MaintenanceCategory } from '../enums/maintenance-category.enum';

export class CreateMaintenanceRequestDto {
@IsOptional()
@IsUUID()
workspaceId?: string;

@IsEnum(MaintenanceCategory)
category: MaintenanceCategory;

@IsString()
@IsNotEmpty()
description: string;

@IsOptional()
@IsString()
imageUrl?: string;
}
30 changes: 30 additions & 0 deletions backend/src/maintenance/dto/maintenance-query.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { IsEnum, IsInt, IsOptional, IsUUID, Min } from 'class-validator';
import { Type } from 'class-transformer';
import { MaintenanceCategory } from '../enums/maintenance-category.enum';
import { MaintenanceStatus } from '../enums/maintenance-status.enum';

export class MaintenanceQueryDto {
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
page?: number = 1;

@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
limit?: number = 20;

@IsOptional()
@IsEnum(MaintenanceStatus)
status?: MaintenanceStatus;

@IsOptional()
@IsEnum(MaintenanceCategory)
category?: MaintenanceCategory;

@IsOptional()
@IsUUID()
workspaceId?: string;
}
7 changes: 7 additions & 0 deletions backend/src/maintenance/dto/update-maintenance-status.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { IsEnum } from 'class-validator';
import { MaintenanceStatus } from '../enums/maintenance-status.enum';

export class UpdateMaintenanceStatusDto {
@IsEnum(MaintenanceStatus)
status: MaintenanceStatus;
}
Loading
Loading