## 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>
144 lines
5.0 KiB
TypeScript
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);
|
|
}
|
|
}
|