Files
CRM_Montzar/apps/api/src/leads/leads.controller.ts
T

395 lines
9.9 KiB
TypeScript

import {
BadRequestException,
Body,
Controller,
Delete,
Get,
Param,
Patch,
Post,
Query,
Res,
StreamableFile,
UploadedFile,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { memoryStorage } from 'multer';
import type { Response } from 'express';
import ExcelJS from 'exceljs';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { RolesGuard } from '../auth/roles.guard';
import { Roles } from '../auth/roles.decorator';
import { CurrentUser } from '../auth/current-user.decorator';
import type { Actor } from '../common/record-scope';
import { OrgModuleGuard } from '../billing/org-module.guard';
import { RequireOrgModule } from '../billing/org-module.decorator';
import { brand } from '@mento/shared';
import {
AssignLeadsDto,
AutoAssignLeadsDto,
BulkLeadStageDto,
ConvertLeadDto,
CreateLeadDto,
CreateLeadStageDto,
ImportLeadsDto,
LeadCallLogDto,
LeadFollowUpDto,
LeadNoteDto,
LeadSmsDto,
MoveLeadDto,
ReorderLeadStagesDto,
UpdateLeadDto,
UpdateLeadStageDto,
} from './dto';
import { LeadStagesService } from './lead-stages.service';
import { LeadsImportService } from './leads-import.service';
import { LeadsService } from './leads.service';
@Controller('leads')
@UseGuards(JwtAuthGuard, OrgModuleGuard)
@RequireOrgModule('leads')
export class LeadsController {
constructor(
private readonly leads: LeadsService,
private readonly stages: LeadStagesService,
private readonly imports: LeadsImportService,
) {}
@Get('stats')
stats(@CurrentUser() user: Actor) {
return this.leads.stats(user);
}
@Get('board')
board(@CurrentUser() user: Actor) {
return this.leads.board(user);
}
@Get('stages')
listStages(@CurrentUser() user: Actor) {
return this.stages.list(user.organizationId);
}
@Post('stages')
@UseGuards(RolesGuard)
@Roles('owner', 'admin')
createStage(@CurrentUser() user: Actor, @Body() dto: CreateLeadStageDto) {
return this.stages.create(user.organizationId, dto);
}
@Patch('stages/reorder')
@UseGuards(RolesGuard)
@Roles('owner', 'admin')
reorderStages(@CurrentUser() user: Actor, @Body() dto: ReorderLeadStagesDto) {
return this.stages.reorder(user.organizationId, dto.stageIds);
}
@Patch('stages/:id')
@UseGuards(RolesGuard)
@Roles('owner', 'admin')
updateStage(
@CurrentUser() user: Actor,
@Param('id') id: string,
@Body() dto: UpdateLeadStageDto,
) {
return this.stages.update(user.organizationId, id, dto);
}
@Delete('stages/:id')
@UseGuards(RolesGuard)
@Roles('owner', 'admin')
async removeStage(@CurrentUser() user: Actor, @Param('id') id: string) {
try {
await this.stages.remove(user.organizationId, id);
return { ok: true };
} catch (e) {
throw new BadRequestException(
e instanceof Error ? e.message : 'حذف مرحله ناموفق بود',
);
}
}
@Get('duplicate-check')
checkDuplicate(
@CurrentUser() user: Actor,
@Query('mobile') mobile: string,
) {
return this.leads.checkDuplicate(user, mobile ?? '');
}
@Get('imports/:id')
getImport(@CurrentUser() user: Actor, @Param('id') id: string) {
return this.imports.getJob(user.organizationId, id);
}
@Post('import')
@UseGuards(RolesGuard)
@Roles('owner', 'admin')
importRows(@CurrentUser() user: Actor, @Body() dto: ImportLeadsDto) {
if (!dto.rows?.length) {
throw new BadRequestException('ردیفی برای ایمپورت نیست');
}
return this.imports.enqueue(
user.organizationId,
user.userId,
dto.rows,
dto.fileName,
);
}
@Post('import/preview')
@UseGuards(RolesGuard)
@Roles('owner', 'admin')
@UseInterceptors(
FileInterceptor('file', {
storage: memoryStorage(),
limits: { fileSize: 15_000_000 },
}),
)
importPreview(@UploadedFile() file?: Express.Multer.File) {
if (!file?.buffer?.length) {
throw new BadRequestException('فایل ارسال نشده');
}
return this.imports.preview(file);
}
@Post('import/file')
@UseGuards(RolesGuard)
@Roles('owner', 'admin')
@UseInterceptors(
FileInterceptor('file', {
storage: memoryStorage(),
limits: { fileSize: 15_000_000 },
}),
)
async importFile(
@CurrentUser() user: Actor,
@UploadedFile() file?: Express.Multer.File,
@Body('mapping') mapping?: string,
) {
if (!file?.buffer?.length) {
throw new BadRequestException('فایل ارسال نشده');
}
const rows = await this.imports.rowsFromFile(file, mapping);
if (!rows.length) {
throw new BadRequestException('ردیفی برای ایمپورت نیست');
}
return this.imports.enqueue(
user.organizationId,
user.userId,
rows,
file.originalname,
);
}
@Get('export')
@UseGuards(RolesGuard)
@Roles('owner', 'admin')
async export(
@CurrentUser() user: Actor,
@Res({ passthrough: true }) res: Response,
) {
const data = await this.leads.list(user, { page: 1, limit: 5000 });
const wb = new ExcelJS.Workbook();
const sheet = wb.addWorksheet('leads');
sheet.addRow([
'نام',
'موبایل',
'مالک',
'شهر',
'منطقه',
'صنف',
'وضعیت',
'کارشناس',
'منبع',
'پیگیری بعدی',
'آخرین تماس',
]);
for (const l of data.items) {
sheet.addRow([
l.title,
l.mobile,
l.contactName,
l.city,
l.region,
l.trade,
l.stageName,
l.assigneeName,
l.source,
l.nextFollowUpAt,
l.lastContactAt,
]);
}
const buffer = Buffer.from(await wb.xlsx.writeBuffer());
res.set({
'Content-Type':
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'Content-Disposition': `attachment; filename="${brand.filePrefix}-leads.xlsx"`,
});
return new StreamableFile(buffer);
}
@Get()
list(
@CurrentUser() user: Actor,
@Query('q') q?: string,
@Query('page') page?: string,
@Query('limit') limit?: string,
@Query('stageId') stageId?: string,
@Query('assigneeId') assigneeId?: string,
@Query('city') city?: string,
@Query('trade') trade?: string,
@Query('source') source?: string,
@Query('unassigned') unassigned?: string,
@Query('archived') archived?: string,
@Query('sort') sort?: string,
) {
return this.leads.list(user, {
q,
page: page ? Number(page) : 1,
limit: limit ? Number(limit) : 20,
stageId,
assigneeId,
city,
trade,
source,
unassigned: unassigned === '1' || unassigned === 'true',
archived: archived === '1' || archived === 'true',
sort,
});
}
@Post()
create(@CurrentUser() user: Actor, @Body() dto: CreateLeadDto) {
return this.leads.create(user, dto);
}
@Post('assign')
@UseGuards(RolesGuard)
@Roles('owner', 'admin')
assign(@CurrentUser() user: Actor, @Body() dto: AssignLeadsDto) {
return this.leads
.assign(user, dto.leadIds, dto.assigneeId)
.then((count) => ({ count }));
}
@Post('bulk-stage')
@UseGuards(RolesGuard)
@Roles('owner', 'admin')
bulkStage(@CurrentUser() user: Actor, @Body() dto: BulkLeadStageDto) {
return this.leads
.bulkStage(user, dto.leadIds, dto.stageId)
.then((count) => ({ count }));
}
@Post('auto-assign')
@UseGuards(RolesGuard)
@Roles('owner', 'admin')
autoAssign(@CurrentUser() user: Actor, @Body() dto: AutoAssignLeadsDto) {
return this.leads.autoAssign(user, dto);
}
@Post('archive')
archive(@CurrentUser() user: Actor, @Body() body: { leadIds: string[] }) {
return this.leads
.archive(user, body.leadIds ?? [])
.then((count) => ({ count }));
}
@Get(':id')
get(@CurrentUser() user: Actor, @Param('id') id: string) {
return this.leads.get(user, id);
}
@Get(':id/logo')
async logo(@CurrentUser() user: Actor, @Param('id') id: string) {
const file = await this.leads.openLogo(user, id);
return new StreamableFile(file.stream, { type: file.mimeType });
}
@Post(':id/logo')
@UseInterceptors(
FileInterceptor('file', {
storage: memoryStorage(),
limits: { fileSize: 2 * 1024 * 1024 },
}),
)
uploadLogo(
@CurrentUser() user: Actor,
@Param('id') id: string,
@UploadedFile() file: Express.Multer.File,
) {
return this.leads.saveLogo(user, id, file);
}
@Delete(':id/logo')
removeLogo(@CurrentUser() user: Actor, @Param('id') id: string) {
return this.leads.removeLogo(user, id);
}
@Patch(':id')
update(
@CurrentUser() user: Actor,
@Param('id') id: string,
@Body() dto: UpdateLeadDto,
) {
return this.leads.update(user, id, dto as unknown as Record<string, unknown>);
}
@Patch(':id/move')
move(
@CurrentUser() user: Actor,
@Param('id') id: string,
@Body() dto: MoveLeadDto,
) {
return this.leads.move(user, id, dto.stageId, dto.position);
}
@Post(':id/sms')
sms(
@CurrentUser() user: Actor,
@Param('id') id: string,
@Body() dto: LeadSmsDto,
) {
return this.leads.sendSms(user, id, dto.text);
}
@Post(':id/call')
logCall(
@CurrentUser() user: Actor,
@Param('id') id: string,
@Body() dto: LeadCallLogDto,
) {
return this.leads.logCall(user, id, dto);
}
@Post(':id/notes')
addNote(
@CurrentUser() user: Actor,
@Param('id') id: string,
@Body() dto: LeadNoteDto,
) {
return this.leads.addNote(user, id, dto.body);
}
@Post(':id/follow-up')
followUp(
@CurrentUser() user: Actor,
@Param('id') id: string,
@Body() dto: LeadFollowUpDto,
) {
return this.leads.setFollowUp(user, id, dto.nextFollowUpAt, dto.body);
}
@Post(':id/convert')
convert(
@CurrentUser() user: Actor,
@Param('id') id: string,
@Body() dto: ConvertLeadDto,
) {
return this.leads.convert(user, id, dto);
}
}