update rezerv

This commit is contained in:
2026-05-31 21:36:19 +03:30
parent 7affab06f1
commit 150b189be5
25 changed files with 1134 additions and 49 deletions
@@ -0,0 +1,74 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Patch,
Post,
Query,
UseGuards,
} from '@nestjs/common';
import { BusinessServiceClassEntity } from '../../entities/business-service-class.entity';
import { UserRole } from '../../entities/user.entity';
import { AuthUser } from '../auth/auth.types';
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 { BusinessServiceClassesService } from './business-service-classes.service';
import { CreateBusinessServiceClassDto } from './dto/create-business-service-class.dto';
import { UpdateBusinessServiceClassDto } from './dto/update-business-service-class.dto';
@Controller('business-service-classes')
export class BusinessServiceClassesController {
constructor(private readonly service: BusinessServiceClassesService) {}
@Get()
findAll(
@Query('businessId') businessId?: string,
@Query('isActive') isActive?: string,
): Promise<BusinessServiceClassEntity[]> {
const parsedIsActive =
typeof isActive === 'string' ? isActive === 'true' : undefined;
return this.service.findAll({ businessId, isActive: parsedIsActive });
}
@Get(':id')
findOne(@Param('id') id: string): Promise<BusinessServiceClassEntity> {
return this.service.findOne(id);
}
@Post()
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.ADMIN, UserRole.BUSINESS_OWNER)
create(
@Body() payload: CreateBusinessServiceClassDto,
@CurrentUser() user: AuthUser,
): Promise<BusinessServiceClassEntity> {
return this.service.create(payload, user);
}
@Patch(':id')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.ADMIN, UserRole.BUSINESS_OWNER)
update(
@Param('id') id: string,
@Body() payload: UpdateBusinessServiceClassDto,
@CurrentUser() user: AuthUser,
): Promise<BusinessServiceClassEntity> {
return this.service.update(id, payload, user);
}
@Delete(':id')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.ADMIN, UserRole.BUSINESS_OWNER)
async remove(
@Param('id') id: string,
@CurrentUser() user: AuthUser,
): Promise<{ success: true }> {
await this.service.remove(id, user);
return { success: true };
}
}
@@ -0,0 +1,18 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BusinessEntity } from '../../entities/business.entity';
import { BusinessServiceClassEntity } from '../../entities/business-service-class.entity';
import { AuthModule } from '../auth/auth.module';
import { BusinessServiceClassesController } from './business-service-classes.controller';
import { BusinessServiceClassesService } from './business-service-classes.service';
@Module({
imports: [
TypeOrmModule.forFeature([BusinessServiceClassEntity, BusinessEntity]),
AuthModule,
],
controllers: [BusinessServiceClassesController],
providers: [BusinessServiceClassesService],
exports: [BusinessServiceClassesService],
})
export class BusinessServiceClassesModule {}
@@ -0,0 +1,168 @@
import {
BadRequestException,
ConflictException,
ForbiddenException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { BusinessEntity } from '../../entities/business.entity';
import { BusinessServiceClassEntity } from '../../entities/business-service-class.entity';
import { UserRole } from '../../entities/user.entity';
import { AuthUser } from '../auth/auth.types';
import { CreateBusinessServiceClassDto } from './dto/create-business-service-class.dto';
import { UpdateBusinessServiceClassDto } from './dto/update-business-service-class.dto';
type FindAllFilters = {
businessId?: string;
isActive?: boolean;
};
@Injectable()
export class BusinessServiceClassesService {
constructor(
@InjectRepository(BusinessServiceClassEntity)
private readonly repo: Repository<BusinessServiceClassEntity>,
@InjectRepository(BusinessEntity)
private readonly businessRepo: Repository<BusinessEntity>,
) {}
async findAll(filters: FindAllFilters): Promise<BusinessServiceClassEntity[]> {
const query = this.repo
.createQueryBuilder('c')
.orderBy('c.sortOrder', 'ASC')
.addOrderBy('c.createdAt', 'ASC');
if (filters.businessId) {
query.andWhere('c.businessId = :businessId', {
businessId: filters.businessId,
});
}
if (typeof filters.isActive === 'boolean') {
query.andWhere('c.isActive = :isActive', { isActive: filters.isActive });
}
return query.getMany();
}
async findOne(id: string): Promise<BusinessServiceClassEntity> {
const row = await this.repo.findOne({ where: { id } });
if (!row) {
throw new NotFoundException('Service class not found');
}
return row;
}
async create(
dto: CreateBusinessServiceClassDto,
actor?: AuthUser,
): Promise<BusinessServiceClassEntity> {
const business = await this.businessRepo.findOne({
where: { id: dto.businessId },
});
if (!business) {
throw new NotFoundException('Business not found');
}
this.assertCanManageBusiness(business, actor);
const dup = await this.repo.findOne({
where: { businessId: dto.businessId, title: dto.title.trim() },
});
if (dup) {
throw new ConflictException('A class with this title already exists');
}
let sortOrder = dto.sortOrder;
if (sortOrder === undefined) {
const max = await this.repo
.createQueryBuilder('c')
.select('MAX(c.sortOrder)', 'max')
.where('c.businessId = :businessId', { businessId: dto.businessId })
.getRawOne<{ max: string | null }>();
sortOrder = (max?.max ? Number.parseInt(max.max, 10) : -1) + 1;
}
const entity = this.repo.create({
businessId: dto.businessId,
title: dto.title.trim(),
description: dto.description?.trim() || null,
sortOrder,
isActive: dto.isActive ?? true,
});
return this.repo.save(entity);
}
async update(
id: string,
dto: UpdateBusinessServiceClassDto,
actor?: AuthUser,
): Promise<BusinessServiceClassEntity> {
const row = await this.findOne(id);
const business = await this.businessRepo.findOne({
where: { id: row.businessId },
});
if (!business) {
throw new NotFoundException('Business not found');
}
this.assertCanManageBusiness(business, actor);
if (dto.title !== undefined) {
const title = dto.title.trim();
if (!title) {
throw new BadRequestException('title cannot be empty');
}
const dup = await this.repo.findOne({
where: { businessId: row.businessId, title },
});
if (dup && dup.id !== row.id) {
throw new ConflictException('A class with this title already exists');
}
row.title = title;
}
if (dto.description !== undefined) {
row.description = dto.description?.trim() || null;
}
if (dto.sortOrder !== undefined) {
row.sortOrder = dto.sortOrder;
}
if (dto.isActive !== undefined) {
row.isActive = dto.isActive;
}
return this.repo.save(row);
}
async remove(id: string, actor?: AuthUser): Promise<void> {
const row = await this.findOne(id);
const business = await this.businessRepo.findOne({
where: { id: row.businessId },
});
if (!business) {
throw new NotFoundException('Business not found');
}
this.assertCanManageBusiness(business, actor);
const result = await this.repo.delete(id);
if (!result.affected) {
throw new NotFoundException('Service class not found');
}
}
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');
}
}
}
@@ -0,0 +1,30 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsBoolean, IsInt, IsOptional, IsString, IsUUID, MaxLength, Min } from 'class-validator';
export class CreateBusinessServiceClassDto {
@ApiProperty({ format: 'uuid' })
@IsUUID()
businessId!: string;
@ApiProperty({ example: 'کلاس ۱ — مینی و کوچک' })
@IsString()
@MaxLength(120)
title!: string;
@ApiPropertyOptional({ nullable: true })
@IsOptional()
@IsString()
@MaxLength(500)
description?: string | null;
@ApiPropertyOptional({ default: 0 })
@IsOptional()
@IsInt()
@Min(0)
sortOrder?: number;
@ApiPropertyOptional({ default: true })
@IsOptional()
@IsBoolean()
isActive?: boolean;
}
@@ -0,0 +1,27 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsBoolean, IsInt, IsOptional, IsString, MaxLength, Min } from 'class-validator';
export class UpdateBusinessServiceClassDto {
@ApiPropertyOptional()
@IsOptional()
@IsString()
@MaxLength(120)
title?: string;
@ApiPropertyOptional({ nullable: true })
@IsOptional()
@IsString()
@MaxLength(500)
description?: string | null;
@ApiPropertyOptional()
@IsOptional()
@IsInt()
@Min(0)
sortOrder?: number;
@ApiPropertyOptional()
@IsOptional()
@IsBoolean()
isActive?: boolean;
}