From 1d685b333809a9459acd5e685cf17de23ca0a9f8 Mon Sep 17 00:00:00 2001 From: Mahdi Date: Sat, 13 Jun 2026 12:39:35 +0330 Subject: [PATCH] edit image business --- .../businesses/business-images.service.ts | 13 +++ .../businesses/businesses.controller.ts | 53 +++++++++-- src/modules/businesses/businesses.service.ts | 90 ++++++++++++++++++- 3 files changed, 145 insertions(+), 11 deletions(-) diff --git a/src/modules/businesses/business-images.service.ts b/src/modules/businesses/business-images.service.ts index ebfc1e9..3badf4b 100644 --- a/src/modules/businesses/business-images.service.ts +++ b/src/modules/businesses/business-images.service.ts @@ -92,6 +92,19 @@ export class BusinessImagesService { return saved; } + async setCover(businessId: string, imageId: string): Promise { + const image = await this.imageRepo.findOne({ + where: { id: imageId, businessId }, + }); + if (!image) { + throw new NotFoundException('Image not found'); + } + + await this.imageRepo.update({ businessId }, { isCover: false }); + image.isCover = true; + return this.imageRepo.save(image); + } + async remove(businessId: string, imageId: string): Promise { const image = await this.imageRepo.findOne({ where: { id: imageId, businessId }, diff --git a/src/modules/businesses/businesses.controller.ts b/src/modules/businesses/businesses.controller.ts index efe2b62..4f588b2 100644 --- a/src/modules/businesses/businesses.controller.ts +++ b/src/modules/businesses/businesses.controller.ts @@ -22,7 +22,6 @@ import { CurrentUser } from '../auth/decorators/current-user.decorator'; import { Roles } from '../auth/decorators/roles.decorator'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { RolesGuard } from '../auth/guards/roles.guard'; -import { BusinessImagesService } from './business-images.service'; import { BusinessesService } from './businesses.service'; import { AssignBusinessOwnerDto } from './dto/assign-business-owner.dto'; import { CreateBusinessDto } from './dto/create-business.dto'; @@ -31,10 +30,7 @@ import { UpdateBusinessDto } from './dto/update-business.dto'; @Controller('businesses') export class BusinessesController { - constructor( - private readonly service: BusinessesService, - private readonly images: BusinessImagesService, - ) {} + constructor(private readonly service: BusinessesService) {} @Get() findAll( @@ -57,6 +53,43 @@ export class BusinessesController { return this.service.findMine(user.id); } + @Post('mine/images') + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles(UserRole.BUSINESS_OWNER) + @UseInterceptors( + FilesInterceptor('files', 10, { + storage: memoryStorage(), + limits: { fileSize: 5 * 1024 * 1024 }, + }), + ) + uploadMyImages( + @UploadedFiles() files: Express.Multer.File[], + @CurrentUser() user: AuthUser, + ): Promise { + return this.service.uploadMyImages(files, user); + } + + @Delete('mine/images/:imageId') + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles(UserRole.BUSINESS_OWNER) + async removeMyImage( + @Param('imageId') imageId: string, + @CurrentUser() user: AuthUser, + ): Promise<{ success: true }> { + await this.service.removeMyImage(imageId, user); + return { success: true }; + } + + @Patch('mine/images/:imageId/cover') + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles(UserRole.BUSINESS_OWNER) + setMyCoverImage( + @Param('imageId') imageId: string, + @CurrentUser() user: AuthUser, + ): Promise { + return this.service.setMyCoverImage(imageId, user); + } + @Get(':id/availability') getAvailability(@Param('id') id: string) { return this.service.getAvailability(id); @@ -87,7 +120,7 @@ export class BusinessesController { @Post(':id/images') @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(UserRole.ADMIN) + @Roles(UserRole.ADMIN, UserRole.BUSINESS_OWNER) @UseInterceptors( FilesInterceptor('files', 10, { storage: memoryStorage(), @@ -97,18 +130,20 @@ export class BusinessesController { uploadImages( @Param('id') id: string, @UploadedFiles() files: Express.Multer.File[], + @CurrentUser() user: AuthUser, ): Promise { - return this.images.upload(id, files); + return this.service.uploadImages(id, files, user); } @Delete(':id/images/:imageId') @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(UserRole.ADMIN) + @Roles(UserRole.ADMIN, UserRole.BUSINESS_OWNER) async removeImage( @Param('id') id: string, @Param('imageId') imageId: string, + @CurrentUser() user: AuthUser, ): Promise<{ success: true }> { - await this.images.remove(id, imageId); + await this.service.removeImage(id, imageId, user); return { success: true }; } diff --git a/src/modules/businesses/businesses.service.ts b/src/modules/businesses/businesses.service.ts index e31fcb8..e322160 100644 --- a/src/modules/businesses/businesses.service.ts +++ b/src/modules/businesses/businesses.service.ts @@ -70,10 +70,12 @@ export class BusinessesService { async findMine(ownerId: string): Promise { const businesses = await this.repo.find({ where: { ownerId }, - relations: { category: true }, + relations: { category: true, images: true }, order: { createdAt: 'DESC' }, }); - await this.attachCoverImages(businesses); + for (const business of businesses) { + business.images = this.sortImages(business.images ?? []); + } return businesses; } @@ -85,6 +87,7 @@ export class BusinessesService { if (!business) { throw new NotFoundException('Business not found'); } + business.images = this.sortImages(business.images ?? []); return business; } @@ -282,6 +285,89 @@ export class BusinessesService { return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`; } + async uploadImages( + id: string, + files: Express.Multer.File[], + actor: AuthUser, + ): Promise { + const business = await this.findOne(id); + this.assertCanManageBusiness(business, actor); + return this.images.upload(id, files); + } + + async uploadMyImages( + files: Express.Multer.File[], + actor: AuthUser, + ): Promise { + const business = await this.requireOwnedBusiness(actor); + return this.images.upload(business.id, files); + } + + async removeImage( + id: string, + imageId: string, + actor: AuthUser, + ): Promise { + const business = await this.findOne(id); + this.assertCanManageBusiness(business, actor); + await this.images.remove(id, imageId); + } + + async removeMyImage(imageId: string, actor: AuthUser): Promise { + const business = await this.requireOwnedBusiness(actor); + await this.images.remove(business.id, imageId); + } + + async setMyCoverImage( + imageId: string, + actor: AuthUser, + ): Promise { + const business = await this.requireOwnedBusiness(actor); + return this.images.setCover(business.id, imageId); + } + + private async requireOwnedBusiness(actor: AuthUser): Promise { + if (actor.role === UserRole.ADMIN) { + throw new ForbiddenException('Use admin image endpoints for this account'); + } + if (actor.role !== UserRole.BUSINESS_OWNER) { + throw new ForbiddenException('Insufficient role'); + } + + const business = await this.repo.findOne({ + where: { ownerId: actor.id }, + order: { createdAt: 'DESC' }, + }); + if (!business) { + throw new NotFoundException('No business linked to your account'); + } + return business; + } + + private sortImages(images: BusinessImageEntity[]): BusinessImageEntity[] { + return [...images].sort((a, b) => { + if (a.isCover !== b.isCover) { + return a.isCover ? -1 : 1; + } + return a.sortOrder - b.sortOrder; + }); + } + + private assertCanManageBusiness( + business: BusinessEntity, + actor?: AuthUser, + ): void { + if (!actor || actor.role === UserRole.ADMIN) { + return; + } + if ( + actor.role !== UserRole.BUSINESS_OWNER || + business.ownerId !== actor.id + ) { + throw new ForbiddenException('You can only manage your own business'); + } + } + private assertCanManageAvailability( business: BusinessEntity, actor?: AuthUser,