diff --git a/src/database/migrations/1720300000000-AddReserveStatusHistory.ts b/src/database/migrations/1720300000000-AddReserveStatusHistory.ts new file mode 100644 index 0000000..2c76fd0 --- /dev/null +++ b/src/database/migrations/1720300000000-AddReserveStatusHistory.ts @@ -0,0 +1,70 @@ +import { + MigrationInterface, + QueryRunner, + Table, + TableForeignKey, + TableIndex, +} from 'typeorm'; + +export class AddReserveStatusHistory1720300000000 implements MigrationInterface { + name = 'AddReserveStatusHistory1720300000000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: 'reserve_status_history', + columns: [ + { + name: 'id', + type: 'uuid', + isPrimary: true, + generationStrategy: 'uuid', + default: 'gen_random_uuid()', + }, + { name: 'reserveId', type: 'uuid' }, + { + name: 'fromStatus', + type: 'reserves_status_enum', + isNullable: true, + }, + { name: 'toStatus', type: 'reserves_status_enum' }, + { name: 'changedByUserId', type: 'uuid', isNullable: true }, + { name: 'createdAt', type: 'timestamp', default: 'now()' }, + ], + }), + true, + ); + + await queryRunner.createForeignKey( + 'reserve_status_history', + new TableForeignKey({ + columnNames: ['reserveId'], + referencedTableName: 'reserves', + referencedColumnNames: ['id'], + onDelete: 'CASCADE', + }), + ); + + await queryRunner.createForeignKey( + 'reserve_status_history', + new TableForeignKey({ + columnNames: ['changedByUserId'], + referencedTableName: 'users', + referencedColumnNames: ['id'], + onDelete: 'SET NULL', + }), + ); + + await queryRunner.createIndex( + 'reserve_status_history', + new TableIndex({ + name: 'IDX_reserve_status_history_reserveId_createdAt', + columnNames: ['reserveId', 'createdAt'], + }), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable('reserve_status_history', true); + } +} diff --git a/src/reserves/entities/reserve-status-history.entity.ts b/src/reserves/entities/reserve-status-history.entity.ts new file mode 100644 index 0000000..c71ec8b --- /dev/null +++ b/src/reserves/entities/reserve-status-history.entity.ts @@ -0,0 +1,51 @@ +import { + Column, + CreateDateColumn, + Entity, + Index, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { User } from '../../users/entities/user.entity'; +import { ReserveStatus } from '../enums/reserve-status.enum'; +import { Reserve } from './reserve.entity'; + +@Entity('reserve_status_history') +@Index(['reserveId', 'createdAt']) +export class ReserveStatusHistory { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + reserveId: string; + + @ManyToOne(() => Reserve, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'reserveId' }) + reserve: Reserve; + + @Column({ + type: 'enum', + enum: ReserveStatus, + enumName: 'reserves_status_enum', + nullable: true, + }) + fromStatus: ReserveStatus | null; + + @Column({ + type: 'enum', + enum: ReserveStatus, + enumName: 'reserves_status_enum', + }) + toStatus: ReserveStatus; + + @Column({ type: 'uuid', nullable: true }) + changedByUserId: string | null; + + @ManyToOne(() => User, { onDelete: 'SET NULL', nullable: true }) + @JoinColumn({ name: 'changedByUserId' }) + changedByUser: User | null; + + @CreateDateColumn() + createdAt: Date; +} diff --git a/src/reserves/reserves.controller.ts b/src/reserves/reserves.controller.ts index 872bc56..467d200 100644 --- a/src/reserves/reserves.controller.ts +++ b/src/reserves/reserves.controller.ts @@ -42,6 +42,15 @@ export class ReservesController { return this.reservesService.findAll(user, query); } + @Roles(UserRole.ADMIN, UserRole.SALON, UserRole.CUSTOMER) + @Get(':id/status-history') + findStatusHistory( + @Param('id', ParseUUIDPipe) id: string, + @CurrentUser() user: AuthUser, + ) { + return this.reservesService.findStatusHistory(id, user); + } + @Roles(UserRole.ADMIN, UserRole.SALON, UserRole.CUSTOMER) @Get(':id') findOne( diff --git a/src/reserves/reserves.module.ts b/src/reserves/reserves.module.ts index 2adf66d..ac692eb 100644 --- a/src/reserves/reserves.module.ts +++ b/src/reserves/reserves.module.ts @@ -8,13 +8,20 @@ import { ServiceSkill } from '../services/entities/service-skill.entity'; import { Service } from '../services/entities/service.entity'; import { StylistsModule } from '../stylists/stylists.module'; import { ReserveItem } from './entities/reserve-item.entity'; +import { ReserveStatusHistory } from './entities/reserve-status-history.entity'; import { Reserve } from './entities/reserve.entity'; import { ReservesController } from './reserves.controller'; import { ReservesService } from './reserves.service'; @Module({ imports: [ - TypeOrmModule.forFeature([Reserve, ReserveItem, Service, ServiceSkill]), + TypeOrmModule.forFeature([ + Reserve, + ReserveItem, + ReserveStatusHistory, + Service, + ServiceSkill, + ]), SalonsModule, CustomersModule, StylistsModule, diff --git a/src/reserves/reserves.service.ts b/src/reserves/reserves.service.ts index ce3ab40..08914c4 100644 --- a/src/reserves/reserves.service.ts +++ b/src/reserves/reserves.service.ts @@ -19,6 +19,7 @@ import { CreateReserveDto } from './dto/create-reserve.dto'; import { FindReservesQueryDto } from './dto/find-reserves-query.dto'; import { UpdateReserveDto } from './dto/update-reserve.dto'; import { ReserveItem } from './entities/reserve-item.entity'; +import { ReserveStatusHistory } from './entities/reserve-status-history.entity'; import { Reserve } from './entities/reserve.entity'; import { ReserveStatus } from './enums/reserve-status.enum'; @@ -29,6 +30,8 @@ export class ReservesService { private readonly reservesRepository: Repository, @InjectRepository(ReserveItem) private readonly reserveItemsRepository: Repository, + @InjectRepository(ReserveStatusHistory) + private readonly reserveStatusHistoryRepository: Repository, @InjectRepository(Service) private readonly servicesRepository: Repository, @InjectRepository(ServiceSkill) @@ -86,6 +89,7 @@ export class ReservesService { }); const saved = await this.reservesRepository.save(reserve); + await this.logStatusChange(saved.id, null, saved.status, user.id); return this.findOne(saved.id, user); } @@ -199,6 +203,9 @@ export class ReservesService { } if (status !== undefined) { + if (status !== reserve.status) { + await this.logStatusChange(reserve.id, reserve.status, status, user.id); + } reserve.status = status; if (status === ReserveStatus.CANCELLED) { reserve.cancellationReason = cancellationReason ?? null; @@ -213,6 +220,34 @@ export class ReservesService { return this.findOne(id, user); } + async findStatusHistory( + id: string, + user: AuthUser, + ): Promise { + await this.findOne(id, user); + + return this.reserveStatusHistoryRepository.find({ + where: { reserveId: id }, + relations: { changedByUser: true }, + order: { createdAt: 'ASC' }, + }); + } + + private async logStatusChange( + reserveId: string, + fromStatus: ReserveStatus | null, + toStatus: ReserveStatus, + changedByUserId: string, + ): Promise { + const entry = this.reserveStatusHistoryRepository.create({ + reserveId, + fromStatus, + toStatus, + changedByUserId, + }); + await this.reserveStatusHistoryRepository.save(entry); + } + private readonly defaultRelations = { salon: true, customer: { user: true },