update concurrentCapacity
This commit is contained in:
@@ -154,6 +154,7 @@ export function generateAvailableSlotStarts(params: {
|
|||||||
slotStepMinutes: number;
|
slotStepMinutes: number;
|
||||||
windows: AvailabilityWindow[];
|
windows: AvailabilityWindow[];
|
||||||
busy: BusyInterval[];
|
busy: BusyInterval[];
|
||||||
|
concurrentCapacity?: number;
|
||||||
now?: Date;
|
now?: Date;
|
||||||
}): string[] {
|
}): string[] {
|
||||||
const {
|
const {
|
||||||
@@ -163,9 +164,12 @@ export function generateAvailableSlotStarts(params: {
|
|||||||
slotStepMinutes,
|
slotStepMinutes,
|
||||||
windows,
|
windows,
|
||||||
busy,
|
busy,
|
||||||
|
concurrentCapacity = 1,
|
||||||
now = new Date(),
|
now = new Date(),
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
|
const capacity = Math.max(1, concurrentCapacity);
|
||||||
|
|
||||||
const dayAnchor = tehranDateTime(dateYmd, 12, 0);
|
const dayAnchor = tehranDateTime(dateYmd, 12, 0);
|
||||||
const dow = tehranDayOfWeek(dayAnchor);
|
const dow = tehranDayOfWeek(dayAnchor);
|
||||||
const dayWindows = windows.filter((w) => w.dayOfWeek === dow);
|
const dayWindows = windows.filter((w) => w.dayOfWeek === dow);
|
||||||
@@ -205,10 +209,10 @@ export function generateAvailableSlotStarts(params: {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const blocked = busy.some((b) =>
|
const overlapCount = busy.filter((b) =>
|
||||||
intervalsOverlap(startMs, endMs, b.startMs, b.endMs),
|
intervalsOverlap(startMs, endMs, b.startMs, b.endMs),
|
||||||
);
|
).length;
|
||||||
if (blocked) {
|
if (overlapCount >= capacity) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddBusinessConcurrentCapacity1789046300000
|
||||||
|
implements MigrationInterface
|
||||||
|
{
|
||||||
|
name = 'AddBusinessConcurrentCapacity1789046300000';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE "businesses"
|
||||||
|
ADD COLUMN IF NOT EXISTS "concurrentCapacity" integer NOT NULL DEFAULT 1
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE "businesses" DROP COLUMN IF EXISTS "concurrentCapacity"
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,6 +49,10 @@ export class BusinessEntity {
|
|||||||
@Column({ default: false })
|
@Column({ default: false })
|
||||||
serviceClassesEnabled!: boolean;
|
serviceClassesEnabled!: boolean;
|
||||||
|
|
||||||
|
/** Max simultaneous bookings per overlapping time window */
|
||||||
|
@Column({ type: 'int', default: 1 })
|
||||||
|
concurrentCapacity!: number;
|
||||||
|
|
||||||
@ManyToOne(() => BusinessCategoryEntity, (category) => category.businesses, {
|
@ManyToOne(() => BusinessCategoryEntity, (category) => category.businesses, {
|
||||||
onDelete: 'RESTRICT',
|
onDelete: 'RESTRICT',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -388,6 +388,7 @@ export class BookingsService {
|
|||||||
|
|
||||||
const windows = await this.loadAvailabilityWindows(businessId);
|
const windows = await this.loadAvailabilityWindows(businessId);
|
||||||
const busy = await this.loadBusyIntervals(businessId, dateYmd);
|
const busy = await this.loadBusyIntervals(businessId, dateYmd);
|
||||||
|
const concurrentCapacity = await this.getConcurrentCapacity(businessId);
|
||||||
|
|
||||||
const slots = generateAvailableSlotStarts({
|
const slots = generateAvailableSlotStarts({
|
||||||
dateYmd,
|
dateYmd,
|
||||||
@@ -396,11 +397,21 @@ export class BookingsService {
|
|||||||
slotStepMinutes: 15,
|
slotStepMinutes: 15,
|
||||||
windows,
|
windows,
|
||||||
busy,
|
busy,
|
||||||
|
concurrentCapacity,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { date: dateYmd, slots, totalDurationMinutes };
|
return { date: dateYmd, slots, totalDurationMinutes };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getConcurrentCapacity(businessId: string): Promise<number> {
|
||||||
|
const business = await this.businessRepo.findOne({
|
||||||
|
where: { id: businessId },
|
||||||
|
select: ['id', 'concurrentCapacity'],
|
||||||
|
});
|
||||||
|
const value = business?.concurrentCapacity ?? 1;
|
||||||
|
return Math.max(1, value);
|
||||||
|
}
|
||||||
|
|
||||||
private async loadAvailabilityWindows(
|
private async loadAvailabilityWindows(
|
||||||
businessId: string,
|
businessId: string,
|
||||||
): Promise<AvailabilityWindow[]> {
|
): Promise<AvailabilityWindow[]> {
|
||||||
@@ -647,6 +658,7 @@ export class BookingsService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const concurrentCapacity = await this.getConcurrentCapacity(businessId);
|
||||||
const slots = generateAvailableSlotStarts({
|
const slots = generateAvailableSlotStarts({
|
||||||
dateYmd,
|
dateYmd,
|
||||||
durationMinutes,
|
durationMinutes,
|
||||||
@@ -657,6 +669,7 @@ export class BookingsService {
|
|||||||
excludeBookingId,
|
excludeBookingId,
|
||||||
excludeBookingGroupId,
|
excludeBookingGroupId,
|
||||||
}),
|
}),
|
||||||
|
concurrentCapacity,
|
||||||
});
|
});
|
||||||
|
|
||||||
const scheduledMs = scheduledAt.getTime();
|
const scheduledMs = scheduledAt.getTime();
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
import {
|
import {
|
||||||
IsBoolean,
|
IsBoolean,
|
||||||
|
IsInt,
|
||||||
IsLatitude,
|
IsLatitude,
|
||||||
IsLongitude,
|
IsLongitude,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
IsUUID,
|
IsUUID,
|
||||||
Matches,
|
Matches,
|
||||||
|
Max,
|
||||||
MaxLength,
|
MaxLength,
|
||||||
|
Min,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
|
|
||||||
export class UpdateBusinessDto {
|
export class UpdateBusinessDto {
|
||||||
@@ -76,4 +79,16 @@ export class UpdateBusinessDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
serviceClassesEnabled?: boolean;
|
serviceClassesEnabled?: boolean;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
example: 1,
|
||||||
|
description: 'How many customers can be served at the same time',
|
||||||
|
minimum: 1,
|
||||||
|
maximum: 20,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
@Max(20)
|
||||||
|
concurrentCapacity?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user