Adding feature to add multiple people to a reservation

This commit is contained in:
Collin Duncan 2024-04-09 16:49:01 +02:00
parent ca3e374d19
commit 73b32402d3
No known key found for this signature in database
8 changed files with 137 additions and 49 deletions

View file

@ -0,0 +1,53 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddingMultipleOpponents1712672829091
implements MigrationInterface
{
name = 'AddingMultipleOpponents1712672829091'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "temporary_reservations" ("id" varchar PRIMARY KEY NOT NULL, "dateRangeStart" datetime NOT NULL, "dateRangeEnd" datetime NOT NULL, "waitListed" boolean NOT NULL DEFAULT (0), "waitingListId" integer, "ownerId" varchar(32) NOT NULL, "opponents" json NOT NULL)`,
)
await queryRunner.query(
`INSERT INTO "temporary_reservations"("id", "dateRangeStart", "dateRangeEnd", "waitListed", "waitingListId", "ownerId", "opponents") SELECT "id", "dateRangeStart", "dateRangeEnd", "waitListed", "waitingListId", "ownerId", json_array(json_object('id', "opponentId", 'name', "opponentName")) as "opponents" FROM "reservations"`,
)
await queryRunner.query(`DROP TABLE "reservations"`)
await queryRunner.query(
`ALTER TABLE "temporary_reservations" RENAME TO "reservations"`,
)
await queryRunner.query(
`CREATE TABLE "temporary_recurring_reservations" ("id" varchar PRIMARY KEY NOT NULL, "dayOfWeek" integer NOT NULL, "timeStart" varchar(6) NOT NULL, "timeEnd" varchar(6) NOT NULL, "ownerId" varchar(32) NOT NULL, "opponents" json NOT NULL)`,
)
await queryRunner.query(
`INSERT INTO "temporary_recurring_reservations"("id", "dayOfWeek", "timeStart", "timeEnd", "ownerId", "opponents") SELECT "id", "dayOfWeek", "timeStart", "timeEnd", "ownerId", json_array(json_object('id', "opponentId", 'name', "opponentName")) as "opponents" FROM "recurring_reservations"`,
)
await queryRunner.query(`DROP TABLE "recurring_reservations"`)
await queryRunner.query(
`ALTER TABLE "temporary_recurring_reservations" RENAME TO "recurring_reservations"`,
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "recurring_reservations" RENAME TO "temporary_recurring_reservations"`,
)
await queryRunner.query(
`CREATE TABLE "recurring_reservations" ("id" varchar PRIMARY KEY NOT NULL, "dayOfWeek" integer NOT NULL, "timeStart" varchar(6) NOT NULL, "timeEnd" varchar(6) NOT NULL, "opponentId" varchar(32) NOT NULL, "opponentName" varchar(255) NOT NULL, "ownerId" varchar(32) NOT NULL)`,
)
await queryRunner.query(
`INSERT INTO "recurring_reservations"("id", "dayOfWeek", "timeStart", "timeEnd", "ownerId", "opponentId", "opponentName") SELECT "id", "dayOfWeek", "timeStart", "timeEnd", "ownerId", "opponents"->0->>'id' as "opponentId", "opponents"->0->>'name' as "opponentName" FROM "temporary_recurring_reservations"`,
)
await queryRunner.query(`DROP TABLE "temporary_recurring_reservations"`)
await queryRunner.query(
`ALTER TABLE "reservations" RENAME TO "temporary_reservations"`,
)
await queryRunner.query(
`CREATE TABLE "reservations" ("id" varchar PRIMARY KEY NOT NULL, "dateRangeStart" datetime NOT NULL, "dateRangeEnd" datetime NOT NULL, "opponentId" varchar(32) NOT NULL, "opponentName" varchar(255) NOT NULL, "waitListed" boolean NOT NULL DEFAULT (0), "waitingListId" integer, "ownerId" varchar(32) NOT NULL)`,
)
await queryRunner.query(
`INSERT INTO "reservations"("id", "dateRangeStart", "dateRangeEnd", "waitListed", "waitingListId", "ownerId", "opponentId", "opponentName") SELECT "id", "dateRangeStart", "dateRangeEnd", "waitListed", "waitingListId", "ownerId", "opponents"->0->>'id' as "opponentId", "opponents"->0->>'name' as "opponentName" FROM "temporary_reservations"`,
)
await queryRunner.query(`DROP TABLE "temporary_reservations"`)
}
}

View file

@ -10,11 +10,26 @@ import {
Query,
UseInterceptors,
} from '@nestjs/common'
import { IsEnum, IsOptional, IsString, Matches } from 'class-validator'
import {
IsArray,
IsEnum,
IsOptional,
IsString,
Matches,
ValidateNested,
} from 'class-validator'
import { DayOfWeek } from './entity'
import { RecurringReservationsService } from './service'
export class CreateRecurringReservationOpponent {
@IsString()
id: string
@IsString()
name: string
}
export class CreateRecurringReservationRequest {
@IsString()
ownerId: string
@ -32,12 +47,9 @@ export class CreateRecurringReservationRequest {
timeEnd?: string
@IsOptional()
@IsString()
opponentId?: string
@IsOptional()
@IsString()
opponentName?: string
@IsArray()
@ValidateNested()
opponents?: CreateRecurringReservationOpponent[]
}
@Controller('recurring-reservations')

View file

@ -3,7 +3,7 @@ import { IsEnum } from 'class-validator'
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'
import dayjs from '../common/dayjs'
import { Reservation } from '../reservations/entity'
import { Opponent, Reservation } from '../reservations/entity'
export enum DayOfWeek {
Monday = 1,
@ -33,11 +33,8 @@ export class RecurringReservation {
@Column('varchar', { length: 6, nullable: false })
timeEnd: string
@Column('varchar', { length: 32, nullable: false })
opponentId: string
@Column('varchar', { length: 255, nullable: false })
opponentName: string
@Column('json', { nullable: false })
opponents: Opponent[]
constructor(partial: Partial<RecurringReservation>) {
Object.assign(this, partial)
@ -63,8 +60,7 @@ export class RecurringReservation {
ownerId: this.ownerId,
dateRangeStart: dateRangeStart,
dateRangeEnd: dateRangeEnd,
opponentId: this.opponentId,
opponentName: this.opponentName,
opponents: this.opponents,
})
return reservation
}

View file

@ -35,23 +35,20 @@ export class RecurringReservationsService {
dayOfWeek,
timeStart,
timeEnd,
opponentId = '-1',
opponentName = 'Gast',
opponents,
}: {
ownerId: string
dayOfWeek: DayOfWeek
timeStart: string
timeEnd?: string
opponentId?: string
opponentName?: string
opponents?: { id: string; name: string }[]
}) {
const recRes = this.recurringReservationsRepository.create({
ownerId,
dayOfWeek,
timeStart,
timeEnd: timeEnd ?? timeStart,
opponentId,
opponentName,
opponents: opponents ?? [{ id: '-1', name: 'Gast' }],
})
return await this.recurringReservationsRepository.save(recRes)
}

View file

@ -13,7 +13,13 @@ import {
UseInterceptors,
} from '@nestjs/common'
import { Transform, TransformationType } from 'class-transformer'
import { IsBoolean, IsOptional, IsString } from 'class-validator'
import {
IsArray,
IsBoolean,
IsOptional,
IsString,
ValidateNested,
} from 'class-validator'
import { Dayjs } from 'dayjs'
import dayjs from '../common/dayjs'
@ -32,6 +38,14 @@ export class GetReservationsQueryParams {
readonly schedulable?: boolean
}
export class CreateReservationOpponent {
@IsString()
id: string
@IsString()
name: string
}
export class CreateReservationRequest {
@IsString()
ownerId: string
@ -62,12 +76,9 @@ export class CreateReservationRequest {
dateRangeEnd?: Dayjs
@IsOptional()
@IsString()
opponentId?: string
@IsOptional()
@IsString()
opponentName?: string
@IsArray()
@ValidateNested()
opponents?: CreateReservationOpponent[]
}
@Controller('reservations')

View file

@ -5,6 +5,11 @@ import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'
import dayjs from '../common/dayjs'
export interface Opponent {
id: string
name: string
}
@Entity({ name: 'reservations' })
export class Reservation {
@PrimaryGeneratedColumn('uuid')
@ -53,11 +58,8 @@ export class Reservation {
})
dateRangeEnd: Dayjs
@Column('varchar', { length: 32, nullable: false })
opponentId: string
@Column('varchar', { length: 255, nullable: false })
opponentName: string
@Column('json', { nullable: false })
opponents: Opponent[]
@Column('boolean', { default: false })
waitListed: boolean

View file

@ -73,21 +73,18 @@ export class ReservationsService {
ownerId,
dateRangeStart,
dateRangeEnd,
opponentId = '-1',
opponentName = 'Gast',
opponents,
}: {
ownerId: string
dateRangeStart: Dayjs
dateRangeEnd?: Dayjs
opponentId?: string
opponentName?: string
opponents?: { id: string; name: string }[]
}) {
const res = this.reservationsRepository.create({
ownerId,
dateRangeStart,
dateRangeEnd: dateRangeEnd ?? dateRangeStart,
opponentId,
opponentName,
opponents: opponents ?? [{ id: '-1', name: 'Gast' }],
})
return await this.reservationsRepository.save(res)
}

View file

@ -9,7 +9,7 @@ import dayjs from '../../common/dayjs'
import { LoggerService } from '../../logger/service.logger'
import { MONITORING_QUEUE_NAME, MonitoringQueue } from '../../monitoring/config'
import { MonitorType } from '../../monitoring/entity'
import { Reservation } from '../../reservations/entity'
import { Opponent, Reservation } from '../../reservations/entity'
import { EmptyPage } from '../pages/empty'
const BAAN_RESERVEREN_ROOT_URL = 'https://squashcity.baanreserveren.nl'
@ -417,14 +417,37 @@ export class BaanReserverenService {
})
}
private async selectOpponent(id: string, name: string) {
private async selectOpponents(opponents: Opponent[]) {
try {
for (let idx = 0; idx < opponents.length; idx += 1) {
const { id, name } = opponents[idx]
await this.selectOpponent(id, name, idx)
}
} catch (error: unknown) {
if (error instanceof RunnerOwnerSearchSelectionError) {
this.loggerService.warn(
'Improper opponents selected, falling back to guest opponent',
)
return await this.selectOpponent('-1', 'Gast', 0)
}
throw error
}
}
private async selectOpponent(id: string, name: string, index: number) {
if (index < 0 || index > 4) {
throw new RunnerOwnerSearchSelectionError(
new Error('Invalid opponent index'),
)
}
const resolvedIndex = index + 2 // players[1] is the owner; players[2,3,4] are the opponents
this.loggerService.debug('Selecting opponent', { id, name })
const player2Search = await this.page
.waitForSelector('input:has(~ select[name="players[2]"])')
const playerSearch = await this.page
.waitForSelector(`input:has(~ select[name="players[${resolvedIndex}]"])`)
.catch((e: Error) => {
throw new RunnerOpponentSearchError(e)
})
await player2Search
await playerSearch
?.type(name, { delay: this.getTypingDelay() })
.catch((e: Error) => {
throw new RunnerOpponentSearchInputError(e)
@ -433,7 +456,7 @@ export class BaanReserverenService {
throw new RunnerOpponentSearchNetworkError(e)
})
await this.page
.$('select.br-user-select[name="players[2]"]')
.$(`select.br-user-select[name="players[${resolvedIndex}]"]`)
.then((d) => d?.select(id))
.catch((e: Error) => {
throw new RunnerOpponentSearchSelectionError(e)
@ -517,10 +540,7 @@ export class BaanReserverenService {
await this.monitorCourtReservations()
await this.selectAvailableTime(reservation)
await this.selectOwner(reservation.ownerId)
await this.selectOpponent(
reservation.opponentId,
reservation.opponentName,
)
await this.selectOpponents(reservation.opponents)
await this.confirmReservation()
} catch (error: unknown) {
await this.handleError()