Add time rezerv
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
|
||||
/** Iranian week: شنبه=0 … جمعه=6 */
|
||||
const TEHRAN_WEEKDAY: Record<string, number> = {
|
||||
Sat: 0,
|
||||
Sun: 1,
|
||||
Mon: 2,
|
||||
Tue: 3,
|
||||
Wed: 4,
|
||||
Thu: 5,
|
||||
Fri: 6,
|
||||
};
|
||||
|
||||
const TIME_RE = /^([01]\d|2[0-3]):([0-5]\d)$/;
|
||||
|
||||
export type AvailabilityWindow = {
|
||||
dayOfWeek: number;
|
||||
startMinute: number;
|
||||
endMinute: number;
|
||||
};
|
||||
|
||||
export type BusyInterval = {
|
||||
startMs: number;
|
||||
endMs: number;
|
||||
};
|
||||
|
||||
export function parseHmToMinute(value: string): number {
|
||||
const match = TIME_RE.exec(String(value).trim());
|
||||
if (!match) {
|
||||
throw new BadRequestException('Time must be HH:mm (24h)');
|
||||
}
|
||||
return Number.parseInt(match[1], 10) * 60 + Number.parseInt(match[2], 10);
|
||||
}
|
||||
|
||||
export function minuteToHm(minute: number): string {
|
||||
const h = Math.floor(minute / 60);
|
||||
const m = minute % 60;
|
||||
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
export function parseGregorianYmd(ymd: string): void {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(ymd)) {
|
||||
throw new BadRequestException('date must be YYYY-MM-DD');
|
||||
}
|
||||
}
|
||||
|
||||
/** Tehran local datetime as UTC Date. */
|
||||
export function tehranDateTime(ymd: string, hour: number, minute: number): Date {
|
||||
parseGregorianYmd(ymd);
|
||||
const hh = String(hour).padStart(2, '0');
|
||||
const mm = String(minute).padStart(2, '0');
|
||||
return new Date(`${ymd}T${hh}:${mm}:00+03:30`);
|
||||
}
|
||||
|
||||
export function tehranYmd(date: Date): string {
|
||||
const parts = new Intl.DateTimeFormat('en-CA', {
|
||||
timeZone: 'Asia/Tehran',
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
}).formatToParts(date);
|
||||
const y = parts.find((p) => p.type === 'year')?.value;
|
||||
const m = parts.find((p) => p.type === 'month')?.value;
|
||||
const d = parts.find((p) => p.type === 'day')?.value;
|
||||
return `${y}-${m}-${d}`;
|
||||
}
|
||||
|
||||
export function tehranDayOfWeek(date: Date): number {
|
||||
const wd = new Intl.DateTimeFormat('en-US', {
|
||||
timeZone: 'Asia/Tehran',
|
||||
weekday: 'short',
|
||||
}).format(date);
|
||||
const mapped = TEHRAN_WEEKDAY[wd];
|
||||
if (mapped === undefined) {
|
||||
throw new BadRequestException('Could not resolve weekday');
|
||||
}
|
||||
return mapped;
|
||||
}
|
||||
|
||||
export function isSameTehranDay(a: Date, b: Date): boolean {
|
||||
return tehranYmd(a) === tehranYmd(b);
|
||||
}
|
||||
|
||||
export function intervalsOverlap(
|
||||
aStart: number,
|
||||
aEnd: number,
|
||||
bStart: number,
|
||||
bEnd: number,
|
||||
): boolean {
|
||||
return aStart < bEnd && bStart < aEnd;
|
||||
}
|
||||
|
||||
export function resolveServiceDurationMinutes(
|
||||
durationMinutes: number | null | undefined,
|
||||
templateDefault?: number | null,
|
||||
): number {
|
||||
if (durationMinutes != null && durationMinutes > 0) {
|
||||
return durationMinutes;
|
||||
}
|
||||
if (templateDefault != null && templateDefault > 0) {
|
||||
return templateDefault;
|
||||
}
|
||||
return 30;
|
||||
}
|
||||
|
||||
export function generateAvailableSlotStarts(params: {
|
||||
dateYmd: string;
|
||||
durationMinutes: number;
|
||||
leadMinutes: number;
|
||||
slotStepMinutes: number;
|
||||
windows: AvailabilityWindow[];
|
||||
busy: BusyInterval[];
|
||||
now?: Date;
|
||||
}): string[] {
|
||||
const {
|
||||
dateYmd,
|
||||
durationMinutes,
|
||||
leadMinutes,
|
||||
slotStepMinutes,
|
||||
windows,
|
||||
busy,
|
||||
now = new Date(),
|
||||
} = params;
|
||||
|
||||
const dayAnchor = tehranDateTime(dateYmd, 12, 0);
|
||||
const dow = tehranDayOfWeek(dayAnchor);
|
||||
const dayWindows = windows.filter((w) => w.dayOfWeek === dow);
|
||||
if (!dayWindows.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const earliestMs = isSameTehranDay(dayAnchor, now)
|
||||
? now.getTime() + leadMinutes * 60 * 1000
|
||||
: tehranDateTime(dateYmd, 0, 0).getTime();
|
||||
|
||||
const slots: string[] = [];
|
||||
const step = Math.max(5, slotStepMinutes);
|
||||
|
||||
for (const window of dayWindows) {
|
||||
for (
|
||||
let startMin = window.startMinute;
|
||||
startMin + durationMinutes <= window.endMinute;
|
||||
startMin += step
|
||||
) {
|
||||
const hour = Math.floor(startMin / 60);
|
||||
const minute = startMin % 60;
|
||||
const startDate = tehranDateTime(dateYmd, hour, minute);
|
||||
const startMs = startDate.getTime();
|
||||
const endMs = startMs + durationMinutes * 60 * 1000;
|
||||
|
||||
if (startMs < earliestMs) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const windowEndMs = tehranDateTime(
|
||||
dateYmd,
|
||||
Math.floor(window.endMinute / 60),
|
||||
window.endMinute % 60,
|
||||
).getTime();
|
||||
if (endMs > windowEndMs) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const blocked = busy.some((b) =>
|
||||
intervalsOverlap(startMs, endMs, b.startMs, b.endMs),
|
||||
);
|
||||
if (blocked) {
|
||||
continue;
|
||||
}
|
||||
|
||||
slots.push(startDate.toISOString());
|
||||
}
|
||||
}
|
||||
|
||||
return slots;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { BusinessAttributeValueEntity } from '../../entities/business-attribute-
|
||||
import { BusinessCategoryEntity } from '../../entities/business-category.entity';
|
||||
import { BusinessServiceEntity } from '../../entities/business-service.entity';
|
||||
import { BusinessImageEntity } from '../../entities/business-image.entity';
|
||||
import { BusinessAvailabilitySlotEntity } from '../../entities/business-availability-slot.entity';
|
||||
import { BusinessEntity } from '../../entities/business.entity';
|
||||
import { ServiceTemplateEntity } from '../../entities/service-template.entity';
|
||||
|
||||
@@ -16,6 +17,7 @@ export async function deleteBusinessCascade(
|
||||
onBeforeDeleteBusiness?: (businessId: string) => Promise<void>,
|
||||
): Promise<void> {
|
||||
await manager.delete(BookingEntity, { businessId });
|
||||
await manager.delete(BusinessAvailabilitySlotEntity, { businessId });
|
||||
await manager.delete(BusinessServiceEntity, { businessId });
|
||||
await manager.delete(BusinessAttributeValueEntity, { businessId });
|
||||
await manager.delete(BusinessImageEntity, { businessId });
|
||||
|
||||
Reference in New Issue
Block a user