import { BadRequestException, Body, Controller, Delete, Get, Param, Patch, Post, Put, 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 { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { RequirePermission, SectionPermissionGuard } from '../auth/section-permission.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 { ActivitiesService } from './activities.service'; import { ActivitiesExcelService } from './activities-excel.service'; import { ActivityStagesService } from './activity-stages.service'; import { ActivityTypesService } from './activity-types.service'; import { CreateActivityBoardDto, CreateActivityDto, CreateActivityStageDto, CreateActivityTypeDto, ImportActivityRowsDto, ListActivitiesQueryDto, ListActivityStagesQueryDto, MoveActivityStageDto, ReorderActivityStagesDto, ReorderActivityTypesDto, UpdateActivityBoardDto, UpdateActivityDto, UpdateActivityPrioritiesDto, UpdateActivityStageDto, UpdateActivityTypeDto, } from './dto'; import { brand } from '@mento/shared'; const excelUpload = FileInterceptor('file', { storage: memoryStorage(), limits: { fileSize: 15_000_000 }, }); @Controller('activities') @UseGuards(JwtAuthGuard, SectionPermissionGuard) @RequirePermission('activities') export class ActivitiesController { constructor( private readonly activitiesService: ActivitiesService, private readonly excelService: ActivitiesExcelService, private readonly stagesService: ActivityStagesService, private readonly typesService: ActivityTypesService, ) {} @Get('summary') summary( @CurrentUser() user: Actor, @Query('desk') desk?: string, ) { const scope = desk === 'mine' || desk === 'team' ? desk : undefined; return this.activitiesService.summary(user.organizationId, user, scope); } @Get('boards') listBoards(@CurrentUser() user: Actor) { return this.stagesService.listBoards(user.organizationId, user); } @Post('boards') @UseGuards(RolesGuard) @Roles('owner', 'admin') createBoard(@CurrentUser() user: Actor, @Body() dto: CreateActivityBoardDto) { return this.stagesService.createBoard(user.organizationId, dto.name, dto.memberIds); } @Patch('boards/:id') @UseGuards(RolesGuard) @Roles('owner', 'admin') updateBoard( @CurrentUser() user: Actor, @Param('id') id: string, @Body() dto: UpdateActivityBoardDto, ) { return this.stagesService.updateBoard(user.organizationId, id, dto); } @Delete('boards/:id') @UseGuards(RolesGuard) @Roles('owner', 'admin') async removeBoard(@CurrentUser() user: Actor, @Param('id') id: string) { await this.stagesService.removeBoard(user.organizationId, id); return { ok: true }; } @Get('stages') listStages(@CurrentUser() user: Actor, @Query() query: ListActivityStagesQueryDto) { return this.stagesService.list(user.organizationId, query.boardId, user); } @Post('stages') @UseGuards(RolesGuard) @Roles('owner', 'admin') createStage(@CurrentUser() user: Actor, @Body() dto: CreateActivityStageDto) { return this.stagesService.create(user.organizationId, dto); } @Patch('stages/reorder') @UseGuards(RolesGuard) @Roles('owner', 'admin') reorderStages(@CurrentUser() user: Actor, @Body() dto: ReorderActivityStagesDto) { return this.stagesService.reorder(user.organizationId, dto.stageIds); } @Patch('stages/:id') @UseGuards(RolesGuard) @Roles('owner', 'admin') updateStage( @CurrentUser() user: Actor, @Param('id') id: string, @Body() dto: UpdateActivityStageDto, ) { return this.stagesService.update(user.organizationId, id, dto); } @Delete('stages/:id') @UseGuards(RolesGuard) @Roles('owner', 'admin') async removeStage(@CurrentUser() user: Actor, @Param('id') id: string) { await this.stagesService.remove(user.organizationId, id); return { ok: true }; } @Get('priorities') listPriorities(@CurrentUser() user: Actor) { return this.typesService.listPriorities(user.organizationId); } @Put('priorities') @UseGuards(RolesGuard) @Roles('owner', 'admin') updatePriorities(@CurrentUser() user: Actor, @Body() dto: UpdateActivityPrioritiesDto) { return this.typesService.updatePriorities(user.organizationId, dto.items); } @Get('types') listTypes(@CurrentUser() user: Actor) { return this.typesService.list(user.organizationId); } @Post('types') @UseGuards(RolesGuard) @Roles('owner', 'admin') createType(@CurrentUser() user: Actor, @Body() dto: CreateActivityTypeDto) { return this.typesService.create(user.organizationId, dto); } @Patch('types/reorder') @UseGuards(RolesGuard) @Roles('owner', 'admin') reorderTypes(@CurrentUser() user: Actor, @Body() dto: ReorderActivityTypesDto) { return this.typesService.reorder(user.organizationId, dto.typeIds); } @Patch('types/:id') @UseGuards(RolesGuard) @Roles('owner', 'admin') updateType( @CurrentUser() user: Actor, @Param('id') id: string, @Body() dto: UpdateActivityTypeDto, ) { return this.typesService.update(user.organizationId, id, dto); } @Delete('types/:id') @UseGuards(RolesGuard) @Roles('owner', 'admin') async removeType(@CurrentUser() user: Actor, @Param('id') id: string) { await this.typesService.remove(user.organizationId, id); return { ok: true }; } @Get('stage-counts') stageCounts( @CurrentUser() user: Actor, @Query() query: ListActivitiesQueryDto, ) { return this.activitiesService.stageCounts( user.organizationId, user, query.filter ?? 'all', query.q, query.desk, { type: query.type, typeDefId: query.typeDefId, status: query.status, assigneeId: query.assigneeId, title: query.title, customer: query.customer, due: query.due, priority: query.priority, boardId: query.boardId, }, ); } @Get() list( @CurrentUser() user: Actor, @Query() query: ListActivitiesQueryDto, ) { return this.activitiesService.list( user.organizationId, user, query.filter ?? 'today', query.q, query.page ?? 1, query.limit ?? 50, { personId: query.personId, companyId: query.companyId, dealId: query.dealId, leadId: query.leadId, }, { from: query.from, to: query.to, }, query.desk, { type: query.type, typeDefId: query.typeDefId, status: query.status, assigneeId: query.assigneeId, title: query.title, customer: query.customer, due: query.due, priority: query.priority, boardId: query.boardId, }, query.stageId ? { stageId: query.stageId } : undefined, ); } /** ترتیب همان لیست میز، برای فعالیت قبلی/بعدی در جزئیات */ @Get('nav-ids') navIds(@CurrentUser() user: Actor, @Query() query: ListActivitiesQueryDto) { return this.activitiesService.orderedIds( user.organizationId, user, query.filter ?? 'today', query.q, { personId: query.personId, companyId: query.companyId, dealId: query.dealId, leadId: query.leadId, }, { from: query.from, to: query.to, }, query.desk, { type: query.type, typeDefId: query.typeDefId, status: query.status, assigneeId: query.assigneeId, title: query.title, customer: query.customer, due: query.due, priority: query.priority, boardId: query.boardId, }, query.stageId ? { stageId: query.stageId } : undefined, ); } @Get('export') async export( @CurrentUser() user: Actor, @Res({ passthrough: true }) res: Response, ) { const buffer = await this.excelService.export(user); res.set({ 'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'Content-Disposition': `attachment; filename="${brand.filePrefix}-activities.xlsx"`, }); return new StreamableFile(buffer); } @Post('import/preview') @UseInterceptors(excelUpload) importPreview(@UploadedFile() file?: Express.Multer.File) { if (!file?.buffer?.length) { throw new BadRequestException('فایل ارسال نشده'); } return this.excelService.preview(file); } @Post('import/prepare') @UseInterceptors(excelUpload) importPrepare( @CurrentUser() user: Actor, @UploadedFile() file?: Express.Multer.File, @Body('mapping') mapping?: string, ) { if (!file?.buffer?.length) { throw new BadRequestException('فایل ارسال نشده'); } return this.excelService.prepare(user, file, mapping); } @Get('import/prepare/:sessionId') importPrepareStatus( @CurrentUser() user: Actor, @Param('sessionId') sessionId: string, ) { return this.excelService.prepareStatus(user, sessionId); } @Post('import/rows') importRows( @CurrentUser() user: Actor, @Body() dto: ImportActivityRowsDto, ) { return this.excelService.importRows(user, dto); } @Post('import') @UseInterceptors(excelUpload) import( @CurrentUser() user: Actor, @UploadedFile() file?: Express.Multer.File, @Body('mapping') mapping?: string, @Body('sessionId') sessionId?: string, @Body('offset') offset?: string, @Body('limit') limit?: string, ) { if (!file?.buffer?.length && !sessionId) { throw new BadRequestException('فایل ارسال نشده'); } return this.excelService.import(user, file, mapping, { sessionId, offset, limit, }); } @Get(':id') get(@CurrentUser() user: Actor, @Param('id') id: string) { return this.activitiesService.get(user.organizationId, id, user); } @Post(':id/images') @UseInterceptors( FileInterceptor('file', { storage: memoryStorage(), limits: { fileSize: 10 * 1024 * 1024 }, }), ) addImage( @CurrentUser() user: Actor, @Param('id') id: string, @UploadedFile() file: Express.Multer.File, ) { return this.activitiesService.addImage( user.organizationId, user.userId, id, file, user, ); } @Get(':id/images/:imageId') async downloadImage( @CurrentUser() user: Actor, @Param('id') id: string, @Param('imageId') imageId: string, ): Promise { const { stream, fileName, mimeType } = await this.activitiesService.openImage( user.organizationId, id, imageId, user, ); const encoded = encodeURIComponent(fileName); return new StreamableFile(stream, { type: mimeType, disposition: `inline; filename="${fileName.replace(/"/g, '')}"; filename*=UTF-8''${encoded}`, }); } @Delete(':id/images/:imageId') async removeImage( @CurrentUser() user: Actor, @Param('id') id: string, @Param('imageId') imageId: string, ) { await this.activitiesService.removeImage(user.organizationId, id, imageId, user); return { ok: true }; } @Post(':id/promote-from-prior') promoteFromPrior(@CurrentUser() user: Actor, @Param('id') id: string) { return this.activitiesService.promoteFromPrior(user, id); } @Post() create(@CurrentUser() user: Actor, @Body() dto: CreateActivityDto) { return this.activitiesService.create( user.organizationId, user.userId, dto, user, ); } @Patch(':id') update( @CurrentUser() user: Actor, @Param('id') id: string, @Body() dto: UpdateActivityDto, ) { return this.activitiesService.update(user.organizationId, id, user, dto); } @Patch(':id/stage') moveStage( @CurrentUser() user: Actor, @Param('id') id: string, @Body() dto: MoveActivityStageDto, ) { return this.activitiesService.moveToStage( user.organizationId, id, user, dto.stageId, dto.position, ); } @Patch(':id/complete') complete( @CurrentUser() user: Actor, @Param('id') id: string, @Query('force') force?: string, @Query('undo') undo?: string, ) { return this.activitiesService.complete(user.organizationId, id, user, { force: force === '1' || force === 'true', undo: undo === '1' || undo === 'true', }); } @Delete(':id') async remove(@CurrentUser() user: Actor, @Param('id') id: string) { await this.activitiesService.remove(user.organizationId, id, user); return { ok: true }; } }