template-saas/apps/backend/src/modules/sales/controllers/leads.controller.ts
Adrian Flores Cortes 529ea53b5e
Some checks are pending
CI / Backend CI (push) Waiting to run
CI / Frontend CI (push) Waiting to run
CI / Security Scan (push) Waiting to run
CI / CI Summary (push) Blocked by required conditions
[SAAS-018] feat: Complete Sales Foundation module implementation
## Backend (NestJS)
- Entities: Lead, Opportunity, PipelineStage, Activity with TypeORM
- Services: LeadsService, OpportunitiesService, PipelineService, ActivitiesService, SalesDashboardService
- Controllers: LeadsController, OpportunitiesController, PipelineController, ActivitiesController, DashboardController
- DTOs: Full set of Create/Update/Convert DTOs with validation
- Tests: 5 test suites with comprehensive coverage

## Frontend (React)
- Pages: /sales, /sales/leads, /sales/leads/[id], /sales/opportunities, /sales/opportunities/[id], /sales/activities
- Components: SalesDashboard, ConversionFunnel, LeadsList, LeadForm, LeadCard, PipelineBoard, OpportunityCard, OpportunityForm, ActivityTimeline, ActivityForm
- Hooks: useLeads, useOpportunities, usePipeline, useActivities, useSalesDashboard
- Services: leads.api, opportunities.api, activities.api, pipeline.api, dashboard.api

## Documentation
- Updated SAAS-018-sales.md with implementation details
- Updated MASTER_INVENTORY.yml - status changed from specified to completed

Story Points: 21
Sprint: 6 - Sales Foundation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 20:49:59 -06:00

144 lines
5.0 KiB
TypeScript

import {
Controller,
Get,
Post,
Patch,
Delete,
Body,
Param,
Query,
ParseUUIDPipe,
UseGuards,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery } from '@nestjs/swagger';
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
import { CurrentTenant } from '../../tenants/decorators/current-tenant.decorator';
import { CurrentUser } from '../../auth/decorators/current-user.decorator';
import { LeadsService, LeadFilters, PaginationOptions } from '../services/leads.service';
import { CreateLeadDto, UpdateLeadDto, ConvertLeadDto } from '../dto';
import { Lead, LeadStatus, LeadSource } from '../entities/lead.entity';
@ApiTags('Sales - Leads')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Controller('sales/leads')
export class LeadsController {
constructor(private readonly leadsService: LeadsService) {}
@Get()
@ApiOperation({ summary: 'Get all leads' })
@ApiQuery({ name: 'page', required: false, type: Number })
@ApiQuery({ name: 'limit', required: false, type: Number })
@ApiQuery({ name: 'status', required: false, enum: LeadStatus })
@ApiQuery({ name: 'source', required: false, enum: LeadSource })
@ApiQuery({ name: 'assigned_to', required: false, type: String })
@ApiQuery({ name: 'search', required: false, type: String })
@ApiQuery({ name: 'sortBy', required: false, type: String })
@ApiQuery({ name: 'sortOrder', required: false, enum: ['ASC', 'DESC'] })
async findAll(
@CurrentTenant() tenantId: string,
@Query('page') page?: number,
@Query('limit') limit?: number,
@Query('status') status?: LeadStatus,
@Query('source') source?: LeadSource,
@Query('assigned_to') assigned_to?: string,
@Query('search') search?: string,
@Query('sortBy') sortBy?: string,
@Query('sortOrder') sortOrder?: 'ASC' | 'DESC',
) {
const filters: LeadFilters = { status, source, assigned_to, search };
const pagination: PaginationOptions = { page, limit, sortBy, sortOrder };
return this.leadsService.findAll(tenantId, filters, pagination);
}
@Get('stats')
@ApiOperation({ summary: 'Get lead statistics' })
async getStats(@CurrentTenant() tenantId: string) {
return this.leadsService.getStats(tenantId);
}
@Get(':id')
@ApiOperation({ summary: 'Get a lead by ID' })
@ApiResponse({ status: 200, description: 'Lead found' })
@ApiResponse({ status: 404, description: 'Lead not found' })
async findOne(
@CurrentTenant() tenantId: string,
@Param('id', ParseUUIDPipe) id: string,
) {
return this.leadsService.findOne(tenantId, id);
}
@Post()
@ApiOperation({ summary: 'Create a new lead' })
@ApiResponse({ status: 201, description: 'Lead created' })
async create(
@CurrentTenant() tenantId: string,
@CurrentUser('id') userId: string,
@Body() dto: CreateLeadDto,
) {
return this.leadsService.create(tenantId, dto, userId);
}
@Patch(':id')
@ApiOperation({ summary: 'Update a lead' })
@ApiResponse({ status: 200, description: 'Lead updated' })
@ApiResponse({ status: 404, description: 'Lead not found' })
async update(
@CurrentTenant() tenantId: string,
@Param('id', ParseUUIDPipe) id: string,
@Body() dto: UpdateLeadDto,
) {
return this.leadsService.update(tenantId, id, dto);
}
@Delete(':id')
@ApiOperation({ summary: 'Delete a lead' })
@ApiResponse({ status: 200, description: 'Lead deleted' })
@ApiResponse({ status: 404, description: 'Lead not found' })
async remove(
@CurrentTenant() tenantId: string,
@Param('id', ParseUUIDPipe) id: string,
) {
await this.leadsService.remove(tenantId, id);
return { message: 'Lead deleted successfully' };
}
@Post(':id/convert')
@ApiOperation({ summary: 'Convert a lead to an opportunity' })
@ApiResponse({ status: 201, description: 'Lead converted to opportunity' })
@ApiResponse({ status: 400, description: 'Lead already converted' })
@ApiResponse({ status: 404, description: 'Lead not found' })
async convertToOpportunity(
@CurrentTenant() tenantId: string,
@Param('id', ParseUUIDPipe) id: string,
@Body() dto: ConvertLeadDto,
) {
return this.leadsService.convertToOpportunity(tenantId, id, dto);
}
@Patch(':id/assign')
@ApiOperation({ summary: 'Assign a lead to a user' })
@ApiResponse({ status: 200, description: 'Lead assigned' })
@ApiResponse({ status: 404, description: 'Lead not found' })
async assignTo(
@CurrentTenant() tenantId: string,
@Param('id', ParseUUIDPipe) id: string,
@Body('user_id', ParseUUIDPipe) userId: string,
) {
return this.leadsService.assignTo(tenantId, id, userId);
}
@Patch(':id/score')
@ApiOperation({ summary: 'Update lead score' })
@ApiResponse({ status: 200, description: 'Score updated' })
@ApiResponse({ status: 400, description: 'Invalid score' })
@ApiResponse({ status: 404, description: 'Lead not found' })
async updateScore(
@CurrentTenant() tenantId: string,
@Param('id', ParseUUIDPipe) id: string,
@Body('score') score: number,
) {
return this.leadsService.updateScore(tenantId, id, score);
}
}