Adding feature to add multiple people to a reservation
This commit is contained in:
parent
ca3e374d19
commit
73b32402d3
8 changed files with 137 additions and 49 deletions
53
database/migrations/1712672829091-AddingMultipleOpponents.ts
Normal file
53
database/migrations/1712672829091-AddingMultipleOpponents.ts
Normal 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"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,11 +10,26 @@ import {
|
||||||
Query,
|
Query,
|
||||||
UseInterceptors,
|
UseInterceptors,
|
||||||
} from '@nestjs/common'
|
} 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 { DayOfWeek } from './entity'
|
||||||
import { RecurringReservationsService } from './service'
|
import { RecurringReservationsService } from './service'
|
||||||
|
|
||||||
|
export class CreateRecurringReservationOpponent {
|
||||||
|
@IsString()
|
||||||
|
id: string
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
export class CreateRecurringReservationRequest {
|
export class CreateRecurringReservationRequest {
|
||||||
@IsString()
|
@IsString()
|
||||||
ownerId: string
|
ownerId: string
|
||||||
|
|
@ -32,12 +47,9 @@ export class CreateRecurringReservationRequest {
|
||||||
timeEnd?: string
|
timeEnd?: string
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsArray()
|
||||||
opponentId?: string
|
@ValidateNested()
|
||||||
|
opponents?: CreateRecurringReservationOpponent[]
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
opponentName?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Controller('recurring-reservations')
|
@Controller('recurring-reservations')
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { IsEnum } from 'class-validator'
|
||||||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'
|
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'
|
||||||
|
|
||||||
import dayjs from '../common/dayjs'
|
import dayjs from '../common/dayjs'
|
||||||
import { Reservation } from '../reservations/entity'
|
import { Opponent, Reservation } from '../reservations/entity'
|
||||||
|
|
||||||
export enum DayOfWeek {
|
export enum DayOfWeek {
|
||||||
Monday = 1,
|
Monday = 1,
|
||||||
|
|
@ -33,11 +33,8 @@ export class RecurringReservation {
|
||||||
@Column('varchar', { length: 6, nullable: false })
|
@Column('varchar', { length: 6, nullable: false })
|
||||||
timeEnd: string
|
timeEnd: string
|
||||||
|
|
||||||
@Column('varchar', { length: 32, nullable: false })
|
@Column('json', { nullable: false })
|
||||||
opponentId: string
|
opponents: Opponent[]
|
||||||
|
|
||||||
@Column('varchar', { length: 255, nullable: false })
|
|
||||||
opponentName: string
|
|
||||||
|
|
||||||
constructor(partial: Partial<RecurringReservation>) {
|
constructor(partial: Partial<RecurringReservation>) {
|
||||||
Object.assign(this, partial)
|
Object.assign(this, partial)
|
||||||
|
|
@ -63,8 +60,7 @@ export class RecurringReservation {
|
||||||
ownerId: this.ownerId,
|
ownerId: this.ownerId,
|
||||||
dateRangeStart: dateRangeStart,
|
dateRangeStart: dateRangeStart,
|
||||||
dateRangeEnd: dateRangeEnd,
|
dateRangeEnd: dateRangeEnd,
|
||||||
opponentId: this.opponentId,
|
opponents: this.opponents,
|
||||||
opponentName: this.opponentName,
|
|
||||||
})
|
})
|
||||||
return reservation
|
return reservation
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,23 +35,20 @@ export class RecurringReservationsService {
|
||||||
dayOfWeek,
|
dayOfWeek,
|
||||||
timeStart,
|
timeStart,
|
||||||
timeEnd,
|
timeEnd,
|
||||||
opponentId = '-1',
|
opponents,
|
||||||
opponentName = 'Gast',
|
|
||||||
}: {
|
}: {
|
||||||
ownerId: string
|
ownerId: string
|
||||||
dayOfWeek: DayOfWeek
|
dayOfWeek: DayOfWeek
|
||||||
timeStart: string
|
timeStart: string
|
||||||
timeEnd?: string
|
timeEnd?: string
|
||||||
opponentId?: string
|
opponents?: { id: string; name: string }[]
|
||||||
opponentName?: string
|
|
||||||
}) {
|
}) {
|
||||||
const recRes = this.recurringReservationsRepository.create({
|
const recRes = this.recurringReservationsRepository.create({
|
||||||
ownerId,
|
ownerId,
|
||||||
dayOfWeek,
|
dayOfWeek,
|
||||||
timeStart,
|
timeStart,
|
||||||
timeEnd: timeEnd ?? timeStart,
|
timeEnd: timeEnd ?? timeStart,
|
||||||
opponentId,
|
opponents: opponents ?? [{ id: '-1', name: 'Gast' }],
|
||||||
opponentName,
|
|
||||||
})
|
})
|
||||||
return await this.recurringReservationsRepository.save(recRes)
|
return await this.recurringReservationsRepository.save(recRes)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,13 @@ import {
|
||||||
UseInterceptors,
|
UseInterceptors,
|
||||||
} from '@nestjs/common'
|
} from '@nestjs/common'
|
||||||
import { Transform, TransformationType } from 'class-transformer'
|
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 'dayjs'
|
||||||
|
|
||||||
import dayjs from '../common/dayjs'
|
import dayjs from '../common/dayjs'
|
||||||
|
|
@ -32,6 +38,14 @@ export class GetReservationsQueryParams {
|
||||||
readonly schedulable?: boolean
|
readonly schedulable?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class CreateReservationOpponent {
|
||||||
|
@IsString()
|
||||||
|
id: string
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
export class CreateReservationRequest {
|
export class CreateReservationRequest {
|
||||||
@IsString()
|
@IsString()
|
||||||
ownerId: string
|
ownerId: string
|
||||||
|
|
@ -62,12 +76,9 @@ export class CreateReservationRequest {
|
||||||
dateRangeEnd?: Dayjs
|
dateRangeEnd?: Dayjs
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsArray()
|
||||||
opponentId?: string
|
@ValidateNested()
|
||||||
|
opponents?: CreateReservationOpponent[]
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
opponentName?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Controller('reservations')
|
@Controller('reservations')
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,11 @@ import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'
|
||||||
|
|
||||||
import dayjs from '../common/dayjs'
|
import dayjs from '../common/dayjs'
|
||||||
|
|
||||||
|
export interface Opponent {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
@Entity({ name: 'reservations' })
|
@Entity({ name: 'reservations' })
|
||||||
export class Reservation {
|
export class Reservation {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
|
@ -53,11 +58,8 @@ export class Reservation {
|
||||||
})
|
})
|
||||||
dateRangeEnd: Dayjs
|
dateRangeEnd: Dayjs
|
||||||
|
|
||||||
@Column('varchar', { length: 32, nullable: false })
|
@Column('json', { nullable: false })
|
||||||
opponentId: string
|
opponents: Opponent[]
|
||||||
|
|
||||||
@Column('varchar', { length: 255, nullable: false })
|
|
||||||
opponentName: string
|
|
||||||
|
|
||||||
@Column('boolean', { default: false })
|
@Column('boolean', { default: false })
|
||||||
waitListed: boolean
|
waitListed: boolean
|
||||||
|
|
|
||||||
|
|
@ -73,21 +73,18 @@ export class ReservationsService {
|
||||||
ownerId,
|
ownerId,
|
||||||
dateRangeStart,
|
dateRangeStart,
|
||||||
dateRangeEnd,
|
dateRangeEnd,
|
||||||
opponentId = '-1',
|
opponents,
|
||||||
opponentName = 'Gast',
|
|
||||||
}: {
|
}: {
|
||||||
ownerId: string
|
ownerId: string
|
||||||
dateRangeStart: Dayjs
|
dateRangeStart: Dayjs
|
||||||
dateRangeEnd?: Dayjs
|
dateRangeEnd?: Dayjs
|
||||||
opponentId?: string
|
opponents?: { id: string; name: string }[]
|
||||||
opponentName?: string
|
|
||||||
}) {
|
}) {
|
||||||
const res = this.reservationsRepository.create({
|
const res = this.reservationsRepository.create({
|
||||||
ownerId,
|
ownerId,
|
||||||
dateRangeStart,
|
dateRangeStart,
|
||||||
dateRangeEnd: dateRangeEnd ?? dateRangeStart,
|
dateRangeEnd: dateRangeEnd ?? dateRangeStart,
|
||||||
opponentId,
|
opponents: opponents ?? [{ id: '-1', name: 'Gast' }],
|
||||||
opponentName,
|
|
||||||
})
|
})
|
||||||
return await this.reservationsRepository.save(res)
|
return await this.reservationsRepository.save(res)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import dayjs from '../../common/dayjs'
|
||||||
import { LoggerService } from '../../logger/service.logger'
|
import { LoggerService } from '../../logger/service.logger'
|
||||||
import { MONITORING_QUEUE_NAME, MonitoringQueue } from '../../monitoring/config'
|
import { MONITORING_QUEUE_NAME, MonitoringQueue } from '../../monitoring/config'
|
||||||
import { MonitorType } from '../../monitoring/entity'
|
import { MonitorType } from '../../monitoring/entity'
|
||||||
import { Reservation } from '../../reservations/entity'
|
import { Opponent, Reservation } from '../../reservations/entity'
|
||||||
import { EmptyPage } from '../pages/empty'
|
import { EmptyPage } from '../pages/empty'
|
||||||
|
|
||||||
const BAAN_RESERVEREN_ROOT_URL = 'https://squashcity.baanreserveren.nl'
|
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 })
|
this.loggerService.debug('Selecting opponent', { id, name })
|
||||||
const player2Search = await this.page
|
const playerSearch = await this.page
|
||||||
.waitForSelector('input:has(~ select[name="players[2]"])')
|
.waitForSelector(`input:has(~ select[name="players[${resolvedIndex}]"])`)
|
||||||
.catch((e: Error) => {
|
.catch((e: Error) => {
|
||||||
throw new RunnerOpponentSearchError(e)
|
throw new RunnerOpponentSearchError(e)
|
||||||
})
|
})
|
||||||
await player2Search
|
await playerSearch
|
||||||
?.type(name, { delay: this.getTypingDelay() })
|
?.type(name, { delay: this.getTypingDelay() })
|
||||||
.catch((e: Error) => {
|
.catch((e: Error) => {
|
||||||
throw new RunnerOpponentSearchInputError(e)
|
throw new RunnerOpponentSearchInputError(e)
|
||||||
|
|
@ -433,7 +456,7 @@ export class BaanReserverenService {
|
||||||
throw new RunnerOpponentSearchNetworkError(e)
|
throw new RunnerOpponentSearchNetworkError(e)
|
||||||
})
|
})
|
||||||
await this.page
|
await this.page
|
||||||
.$('select.br-user-select[name="players[2]"]')
|
.$(`select.br-user-select[name="players[${resolvedIndex}]"]`)
|
||||||
.then((d) => d?.select(id))
|
.then((d) => d?.select(id))
|
||||||
.catch((e: Error) => {
|
.catch((e: Error) => {
|
||||||
throw new RunnerOpponentSearchSelectionError(e)
|
throw new RunnerOpponentSearchSelectionError(e)
|
||||||
|
|
@ -517,10 +540,7 @@ export class BaanReserverenService {
|
||||||
await this.monitorCourtReservations()
|
await this.monitorCourtReservations()
|
||||||
await this.selectAvailableTime(reservation)
|
await this.selectAvailableTime(reservation)
|
||||||
await this.selectOwner(reservation.ownerId)
|
await this.selectOwner(reservation.ownerId)
|
||||||
await this.selectOpponent(
|
await this.selectOpponents(reservation.opponents)
|
||||||
reservation.opponentId,
|
|
||||||
reservation.opponentName,
|
|
||||||
)
|
|
||||||
await this.confirmReservation()
|
await this.confirmReservation()
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
await this.handleError()
|
await this.handleError()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue