From a8c255a3a44519b421d0f6ed69520da4e937be9a Mon Sep 17 00:00:00 2001 From: Mahdi Date: Mon, 25 May 2026 20:06:10 +0330 Subject: [PATCH] edit time rezerv --- src/common/utils/booking-schedule.util.ts | 46 ++++++++++++++++++- ...3000000-CreateBusinessAvailabilitySlots.ts | 32 +++++++++++++ .../businesses/dto/set-availability.dto.ts | 6 ++- 3 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 src/database/migrations/1789043000000-CreateBusinessAvailabilitySlots.ts diff --git a/src/common/utils/booking-schedule.util.ts b/src/common/utils/booking-schedule.util.ts index eb1a880..928069f 100644 --- a/src/common/utils/booking-schedule.util.ts +++ b/src/common/utils/booking-schedule.util.ts @@ -12,6 +12,49 @@ const TEHRAN_WEEKDAY: Record = { }; const TIME_RE = /^([01]\d|2[0-3]):([0-5]\d)$/; +const PERSIAN_DIGITS = '۰۱۲۳۴۵۶۷۸۹'; +const ARABIC_DIGITS = '٠١٢٣٤٥٦٧٨٩'; + +function toAsciiDigits(value: string): string { + return value.replace(/./g, (ch) => { + const pi = PERSIAN_DIGITS.indexOf(ch); + if (pi >= 0) return String(pi); + const ai = ARABIC_DIGITS.indexOf(ch); + if (ai >= 0) return String(ai); + return ch; + }); +} + +/** Accepts browser variants (9:00, 09:00:00, Persian digits) → HH:mm */ +export function normalizeHmTimeInput(value: unknown): string | null { + if (value == null || value === '') return null; + let raw = toAsciiDigits(String(value).trim()).normalize('NFKC'); + raw = raw.replace(/[\u200e\u200f\u202a\u202c]/g, ''); + + const ampm = /^(\d{1,2}):(\d{1,2})(?::\d{1,2})?\s*([ap])(?:\.)?m?\.?$/i.exec( + raw, + ); + if (ampm) { + let h = Number.parseInt(ampm[1], 10); + const m = Number.parseInt(ampm[2], 10); + const isPm = ampm[3].toLowerCase() === 'p'; + if (h === 12) h = isPm ? 12 : 0; + else if (isPm) h += 12; + if (h < 0 || h > 23 || m < 0 || m > 59) return null; + return minuteToHm(h * 60 + m); + } + + const match = /^(\d{1,2}):(\d{1,2})(?::\d{1,2})?$/.exec(raw); + if (!match) return null; + let h = Number.parseInt(match[1], 10); + const m = Number.parseInt(match[2], 10); + if (h === 24 && m === 0) { + h = 23; + return '23:59'; + } + if (h < 0 || h > 23 || m < 0 || m > 59) return null; + return minuteToHm(h * 60 + m); +} export type AvailabilityWindow = { dayOfWeek: number; @@ -25,7 +68,8 @@ export type BusyInterval = { }; export function parseHmToMinute(value: string): number { - const match = TIME_RE.exec(String(value).trim()); + const normalized = normalizeHmTimeInput(value); + const match = TIME_RE.exec(normalized ?? ''); if (!match) { throw new BadRequestException('Time must be HH:mm (24h)'); } diff --git a/src/database/migrations/1789043000000-CreateBusinessAvailabilitySlots.ts b/src/database/migrations/1789043000000-CreateBusinessAvailabilitySlots.ts new file mode 100644 index 0000000..4d73c77 --- /dev/null +++ b/src/database/migrations/1789043000000-CreateBusinessAvailabilitySlots.ts @@ -0,0 +1,32 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateBusinessAvailabilitySlots1789043000000 + implements MigrationInterface +{ + name = 'CreateBusinessAvailabilitySlots1789043000000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE IF NOT EXISTS "business_availability_slots" ( + "id" uuid NOT NULL DEFAULT gen_random_uuid(), + "businessId" uuid NOT NULL, + "dayOfWeek" smallint NOT NULL, + "startMinute" integer NOT NULL, + "endMinute" integer NOT NULL, + "createdAt" TIMESTAMP NOT NULL DEFAULT now(), + "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "PK_business_availability_slots" PRIMARY KEY ("id"), + CONSTRAINT "FK_availability_business" FOREIGN KEY ("businessId") + REFERENCES "businesses"("id") ON DELETE CASCADE + ) + `); + await queryRunner.query(` + CREATE INDEX IF NOT EXISTS "IDX_availability_business_day" + ON "business_availability_slots" ("businessId", "dayOfWeek") + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE IF EXISTS "business_availability_slots"`); + } +} diff --git a/src/modules/businesses/dto/set-availability.dto.ts b/src/modules/businesses/dto/set-availability.dto.ts index 6ae872f..09871bb 100644 --- a/src/modules/businesses/dto/set-availability.dto.ts +++ b/src/modules/businesses/dto/set-availability.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; +import { Transform, Type } from 'class-transformer'; import { ArrayMaxSize, IsArray, @@ -10,20 +10,24 @@ import { Min, ValidateNested, } from 'class-validator'; +import { normalizeHmTimeInput } from '../../../common/utils/booking-schedule.util'; export class AvailabilitySlotDto { @ApiProperty({ description: '0=شنبه … 6=جمعه', minimum: 0, maximum: 6 }) + @Type(() => Number) @IsInt() @Min(0) @Max(6) dayOfWeek!: number; @ApiProperty({ example: '09:00' }) + @Transform(({ value }) => normalizeHmTimeInput(value) ?? value) @IsString() @Matches(/^([01]\d|2[0-3]):[0-5]\d]$/) startTime!: string; @ApiProperty({ example: '17:00' }) + @Transform(({ value }) => normalizeHmTimeInput(value) ?? value) @IsString() @Matches(/^([01]\d|2[0-3]):[0-5]\d]$/) endTime!: string;