add calendar module
This commit is contained in:
parent
c28431e80c
commit
085605f85c
113
server/api.http
113
server/api.http
@ -39,9 +39,114 @@ Content-Type: application/json
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
### GET
|
### GET request with parameter
|
||||||
|
GET {{apiBaseUrl}}/calendar/test?startDate=2011-10-05T14:48:00.000Z&endDate=2025-11-20T17:00:09.234Z
|
||||||
|
Accept: application/json
|
||||||
|
Authorization: Bearer {{auth_token}}
|
||||||
|
|
||||||
# curl -i http://httpbin.org/ip
|
|
||||||
GET http://httpbin.org/ip
|
|
||||||
|
|
||||||
###
|
### GET request with parameter
|
||||||
|
GET {{apiBaseUrl}}/calendar/events/1
|
||||||
|
Accept: application/json
|
||||||
|
Authorization: Bearer {{auth_token}}
|
||||||
|
|
||||||
|
|
||||||
|
### GET request with parameter
|
||||||
|
GET {{apiBaseUrl}}/calendar?startDate=2011-10-05T14:48:00.000Z&endDate=2025-12-20T17:00:09.234Z
|
||||||
|
Accept: application/json
|
||||||
|
Authorization: Bearer {{auth_token}}
|
||||||
|
|
||||||
|
|
||||||
|
### Post Creating a Single, Non-Recurring Event
|
||||||
|
POST {{apiBaseUrl}}/calendar/events
|
||||||
|
Accept: application/json
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer {{auth_token}}
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
"title": "Project Deadline Discussion",
|
||||||
|
"description": "Final review of the Q4 project deliverables.",
|
||||||
|
"startTime": "2025-11-28T14:00:00Z",
|
||||||
|
"endTime": "2025-11-28T15:30:00Z",
|
||||||
|
"timezone": "Europe/Berlin",
|
||||||
|
"isRecurring": false,
|
||||||
|
"eventTypeId": 1
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
### Creating a Recurring Event
|
||||||
|
POST {{apiBaseUrl}}/calendar/events
|
||||||
|
Accept: application/json
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer {{auth_token}}
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
"title": "Daily Team Stand-up",
|
||||||
|
"startTime": "2025-12-01T09:00:00Z",
|
||||||
|
"endTime": "2025-12-01T09:15:00Z",
|
||||||
|
"timezone": "Europe/Budapest",
|
||||||
|
"isRecurring": true,
|
||||||
|
"recurrenceRule": {
|
||||||
|
"frequency": "WEEKLY",
|
||||||
|
"interval": 1,
|
||||||
|
"byDay": "MO,TU,WE,TH,FR",
|
||||||
|
"count": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
### Cancelling a Single Occurrence of a Recurring Event
|
||||||
|
POST {{apiBaseUrl}}/calendar/events/1/exceptions
|
||||||
|
Accept: application/json
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer {{auth_token}}
|
||||||
|
|
||||||
|
{
|
||||||
|
"originalStartTime": "2025-12-03T09:00:00Z",
|
||||||
|
"isCancelled": true
|
||||||
|
}
|
||||||
|
|
||||||
|
### Rescheduling/Modifying a Single Occurrence
|
||||||
|
POST {{apiBaseUrl}}/calendar/events/1/exceptions
|
||||||
|
Accept: application/json
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer {{auth_token}}
|
||||||
|
|
||||||
|
{
|
||||||
|
"originalStartTime": "2025-12-05T09:00:00Z",
|
||||||
|
"title": "Daily Stand-up - Special Demo",
|
||||||
|
"newStartTime": "2025-12-05T15:00:00Z",
|
||||||
|
"newEndTime": "2025-12-05T15:30:00Z"
|
||||||
|
}
|
||||||
|
|
||||||
|
### Rescheduling/Modifying a Single Occurrence
|
||||||
|
POST {{apiBaseUrl}}/calendar/events/1/exceptions
|
||||||
|
Accept: application/json
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer {{auth_token}}
|
||||||
|
|
||||||
|
{
|
||||||
|
"originalStartTime": "2025-12-05T09:00:00Z",
|
||||||
|
"title": "Daily Stand-up - Special Demo",
|
||||||
|
"newStartTime": "2025-12-05T15:00:00Z",
|
||||||
|
"newEndTime": "2025-12-05T15:30:00Z"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
### Updating an Entire Event Series
|
||||||
|
PATCH {{apiBaseUrl}}/calendar/events/1
|
||||||
|
Accept: application/json
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer {{auth_token}}
|
||||||
|
|
||||||
|
{
|
||||||
|
"title": "Agile Team Sync"
|
||||||
|
}
|
||||||
|
|
||||||
|
### Updating an Entire Event Series
|
||||||
|
DELETE {{apiBaseUrl}}/calendar/events/1
|
||||||
|
Accept: application/json
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer {{auth_token}}
|
||||||
|
|||||||
18
server/package-lock.json
generated
18
server/package-lock.json
generated
@ -25,6 +25,7 @@
|
|||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"rrule": "^2.8.1",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"typeorm": "^0.3.27"
|
"typeorm": "^0.3.27"
|
||||||
},
|
},
|
||||||
@ -39,6 +40,7 @@
|
|||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/node": "^22.10.7",
|
"@types/node": "^22.10.7",
|
||||||
"@types/passport-jwt": "^4.0.0",
|
"@types/passport-jwt": "^4.0.0",
|
||||||
|
"@types/rrule": "^2.1.7",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"eslint": "^9.18.0",
|
"eslint": "^9.18.0",
|
||||||
"eslint-config-prettier": "^10.0.1",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
@ -3168,6 +3170,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/rrule": {
|
||||||
|
"version": "2.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/rrule/-/rrule-2.1.7.tgz",
|
||||||
|
"integrity": "sha512-GoyBTiltOwZjHHIW3UoT2pZ+OWzIw/WcQB7bnu08/NKdoQgTWdVpM1hOUqmUmHERuOFkm8QCxpQlz/Mli7VewA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/send": {
|
"node_modules/@types/send": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
|
||||||
@ -9528,6 +9537,15 @@
|
|||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rrule": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rrule/-/rrule-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-hM3dHSBMeaJ0Ktp7W38BJZ7O1zOgaFEsn41PDk+yHoEtfLV+PoJt9E9xAlZiWgf/iqEqionN0ebHFZIDAp+iGw==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/run-parallel": {
|
"node_modules/run-parallel": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||||
|
|||||||
@ -39,6 +39,7 @@
|
|||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"rrule": "^2.8.1",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"typeorm": "^0.3.27"
|
"typeorm": "^0.3.27"
|
||||||
},
|
},
|
||||||
@ -53,6 +54,7 @@
|
|||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/node": "^22.10.7",
|
"@types/node": "^22.10.7",
|
||||||
"@types/passport-jwt": "^4.0.0",
|
"@types/passport-jwt": "^4.0.0",
|
||||||
|
"@types/rrule": "^2.1.7",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"eslint": "^9.18.0",
|
"eslint": "^9.18.0",
|
||||||
"eslint-config-prettier": "^10.0.1",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
|
|||||||
@ -19,8 +19,10 @@ import { UserGroupsModule } from './user-group/user-group.module';
|
|||||||
import { UserRolesModule } from './user-role/user-role.module';
|
import { UserRolesModule } from './user-role/user-role.module';
|
||||||
import { RecurrenceRule } from './entity/recurrence-rule.entity';
|
import { RecurrenceRule } from './entity/recurrence-rule.entity';
|
||||||
import { RecurrenceRulesModule } from './recurrence-rule/recurrence-rules.module';
|
import { RecurrenceRulesModule } from './recurrence-rule/recurrence-rules.module';
|
||||||
import { EventException } from "./entity/event-exception.entity";
|
import { EventException } from './entity/event-exception.entity';
|
||||||
import { EventExceptionsModule } from "./event-exception/event-exceptions.module";
|
import { EventExceptionsModule } from './event-exception/event-exceptions.module';
|
||||||
|
import { CalendarModule } from './calendar/calendar.module';
|
||||||
|
import { Booking } from './entity/booking.entity';
|
||||||
|
|
||||||
const moduleTypeOrm = TypeOrmModule.forRootAsync({
|
const moduleTypeOrm = TypeOrmModule.forRootAsync({
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
@ -41,8 +43,9 @@ const moduleTypeOrm = TypeOrmModule.forRootAsync({
|
|||||||
Product,
|
Product,
|
||||||
Event,
|
Event,
|
||||||
RecurrenceRule,
|
RecurrenceRule,
|
||||||
EventException
|
EventException,
|
||||||
],
|
Booking
|
||||||
|
],
|
||||||
logging: true,
|
logging: true,
|
||||||
// synchronize: true,
|
// synchronize: true,
|
||||||
};
|
};
|
||||||
@ -62,8 +65,9 @@ const moduleTypeOrm = TypeOrmModule.forRootAsync({
|
|||||||
UserGroupsModule,
|
UserGroupsModule,
|
||||||
UserRolesModule,
|
UserRolesModule,
|
||||||
RecurrenceRulesModule,
|
RecurrenceRulesModule,
|
||||||
EventExceptionsModule
|
EventExceptionsModule,
|
||||||
],
|
CalendarModule,
|
||||||
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
providers: [AppService],
|
||||||
})
|
})
|
||||||
|
|||||||
86
server/src/calendar/calendar.controller.ts
Normal file
86
server/src/calendar/calendar.controller.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
Param,
|
||||||
|
Patch,
|
||||||
|
Delete,
|
||||||
|
Query,
|
||||||
|
ParseIntPipe,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { CalendarService } from './calendar.service';
|
||||||
|
import { GetCalendarDto } from './dto/get-calendar.dto';
|
||||||
|
import { CreateEventDto } from './dto/create-event.dto';
|
||||||
|
import { CreateExceptionDto } from './dto/create-exception.dto';
|
||||||
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
|
import { RolesGuard } from '../auth/roles.guard';
|
||||||
|
import { Roles } from '../auth/roles.decorator';
|
||||||
|
import { Role } from '../auth/role.enum';
|
||||||
|
|
||||||
|
@Controller('calendar')
|
||||||
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
|
@Roles(Role.Admin)
|
||||||
|
export class CalendarController {
|
||||||
|
constructor(private readonly calendarService: CalendarService) {}
|
||||||
|
|
||||||
|
@Get('test')
|
||||||
|
getCalendarTest(
|
||||||
|
@Query('startDate') startDate: string,
|
||||||
|
@Query('endDate') endDate: string,
|
||||||
|
) {
|
||||||
|
console.log('--- TEST ENDPOINT ---');
|
||||||
|
console.log('startDate received:', startDate, '| Type:', typeof startDate);
|
||||||
|
console.log('endDate received:', endDate, '| Type:', typeof endDate);
|
||||||
|
return {
|
||||||
|
message: 'Test successful. Check your server console logs.',
|
||||||
|
received: {
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// The primary endpoint to get event occurrences
|
||||||
|
@Get()
|
||||||
|
getCalendarEvents(@Query() getCalendarDto: GetCalendarDto) {
|
||||||
|
return this.calendarService.getEventsInRange(
|
||||||
|
getCalendarDto.startDate,
|
||||||
|
getCalendarDto.endDate,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard CRUD endpoints for managing event entities
|
||||||
|
@Post('events')
|
||||||
|
createEvent(@Body() createEventDto: CreateEventDto) {
|
||||||
|
return this.calendarService.createEvent(createEventDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('events/:id')
|
||||||
|
getEventById(@Param('id', ParseIntPipe) id: number) {
|
||||||
|
return this.calendarService.getEventById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch('events/:id')
|
||||||
|
updateEvent(
|
||||||
|
@Param('id', ParseIntPipe) id: number,
|
||||||
|
@Body() updateEventDto: CreateEventDto,
|
||||||
|
) {
|
||||||
|
return this.calendarService.updateEvent(id, updateEventDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('events/:id')
|
||||||
|
deleteEvent(@Param('id', ParseIntPipe) id: number) {
|
||||||
|
return this.calendarService.deleteEvent(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Endpoint for creating exceptions to a recurring event
|
||||||
|
@Post('events/:id/exceptions')
|
||||||
|
createException(
|
||||||
|
@Param('id', ParseIntPipe) eventId: number,
|
||||||
|
@Body() createExceptionDto: CreateExceptionDto,
|
||||||
|
) {
|
||||||
|
return this.calendarService.createException(eventId, createExceptionDto);
|
||||||
|
}
|
||||||
|
}
|
||||||
14
server/src/calendar/calendar.module.ts
Normal file
14
server/src/calendar/calendar.module.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { CalendarService } from './calendar.service';
|
||||||
|
import { CalendarController } from './calendar.controller';
|
||||||
|
import { RecurrenceRule } from '../entity/recurrence-rule.entity';
|
||||||
|
import { EventException } from '../entity/event-exception.entity';
|
||||||
|
import { Event } from '../entity/event.entity';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([RecurrenceRule, EventException, Event])],
|
||||||
|
controllers: [CalendarController],
|
||||||
|
providers: [CalendarService],
|
||||||
|
})
|
||||||
|
export class CalendarModule {}
|
||||||
188
server/src/calendar/calendar.service.ts
Normal file
188
server/src/calendar/calendar.service.ts
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository, Between } from 'typeorm';
|
||||||
|
import { RRule, Weekday } from 'rrule'; // Corrected import
|
||||||
|
|
||||||
|
import { Event } from '../entity/event.entity';
|
||||||
|
import { EventException } from '../entity/event-exception.entity';
|
||||||
|
import { RecurrenceRule } from '../entity/recurrence-rule.entity';
|
||||||
|
import { CreateEventDto } from './dto/create-event.dto';
|
||||||
|
import { CreateExceptionDto } from './dto/create-exception.dto';
|
||||||
|
|
||||||
|
type CalendarEventDto = Event & {
|
||||||
|
isModified?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Type-Safe Maps ---
|
||||||
|
const frequencyMap: Record<string, RRule.Frequency> = {
|
||||||
|
YEARLY: RRule.YEARLY,
|
||||||
|
MONTHLY: RRule.MONTHLY,
|
||||||
|
WEEKLY: RRule.WEEKLY,
|
||||||
|
DAILY: RRule.DAILY,
|
||||||
|
};
|
||||||
|
|
||||||
|
const weekdayMap: Record<string, RRule.Weekday> = {
|
||||||
|
SU: RRule.SU,
|
||||||
|
MO: RRule.MO,
|
||||||
|
TU: RRule.TU,
|
||||||
|
WE: RRule.WE,
|
||||||
|
TH: RRule.TH,
|
||||||
|
FR: RRule.FR,
|
||||||
|
SA: RRule.SA,
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CalendarService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Event)
|
||||||
|
private readonly eventRepository: Repository<Event>,
|
||||||
|
@InjectRepository(RecurrenceRule)
|
||||||
|
private readonly recurrenceRuleRepository: Repository<RecurrenceRule>,
|
||||||
|
@InjectRepository(EventException)
|
||||||
|
private readonly eventExceptionRepository: Repository<EventException>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getEventsInRange(startDate: Date, endDate: Date): Promise<any[]> {
|
||||||
|
// 1. Fetch single events
|
||||||
|
const singleEvents = await this.eventRepository.find({
|
||||||
|
where: {
|
||||||
|
isRecurring: false,
|
||||||
|
startTime: Between(startDate, endDate),
|
||||||
|
},
|
||||||
|
relations: ['eventType'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Fetch recurring event templates
|
||||||
|
const recurringTemplates = await this.eventRepository.find({
|
||||||
|
where: { isRecurring: true },
|
||||||
|
relations: ['recurrenceRule', 'exceptions', 'eventType'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const recurringOccurrences: CalendarEventDto[] = [];
|
||||||
|
|
||||||
|
// 3. Expand recurring events
|
||||||
|
for (const event of recurringTemplates) {
|
||||||
|
if (!event.recurrenceRule) continue;
|
||||||
|
|
||||||
|
const duration = event.endTime.getTime() - event.startTime.getTime();
|
||||||
|
|
||||||
|
const freq = frequencyMap[event.recurrenceRule.frequency];
|
||||||
|
const byweekday = event.recurrenceRule.byDay
|
||||||
|
?.split(',')
|
||||||
|
.map((day) => weekdayMap[day])
|
||||||
|
.filter((day): day is Weekday => !!day);
|
||||||
|
|
||||||
|
if (freq === undefined) {
|
||||||
|
console.error(
|
||||||
|
`Invalid frequency for event ID ${event.id}: ${event.recurrenceRule.frequency}`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rrule = new RRule({
|
||||||
|
freq: freq,
|
||||||
|
interval: event.recurrenceRule.interval,
|
||||||
|
dtstart: event.startTime,
|
||||||
|
until: event.recurrenceRule.endDate,
|
||||||
|
count: event.recurrenceRule.count,
|
||||||
|
byweekday: byweekday?.length > 0 ? byweekday : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const occurrences = rrule.between(startDate, endDate);
|
||||||
|
|
||||||
|
for (const occurrenceDate of occurrences) {
|
||||||
|
const exception = event.exceptions.find(
|
||||||
|
(ex) => ex.originalStartTime.getTime() === occurrenceDate.getTime(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (exception) {
|
||||||
|
if (exception.isCancelled) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
recurringOccurrences.push({
|
||||||
|
...event,
|
||||||
|
id: event.id,
|
||||||
|
title: exception.title || event.title,
|
||||||
|
description: exception.description || event.description,
|
||||||
|
startTime: exception.newStartTime,
|
||||||
|
endTime: exception.newEndTime,
|
||||||
|
isModified: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
recurringOccurrences.push({
|
||||||
|
...event,
|
||||||
|
startTime: occurrenceDate,
|
||||||
|
endTime: new Date(occurrenceDate.getTime() + duration),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const allEvents = [...singleEvents, ...recurringOccurrences];
|
||||||
|
return allEvents.sort(
|
||||||
|
(a, b) => a.startTime.getTime() - b.startTime.getTime(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Other service methods (createEvent, etc.) remain unchanged ---
|
||||||
|
|
||||||
|
async createEvent(createEventDto: CreateEventDto): Promise<Event> {
|
||||||
|
const { recurrenceRule, ...eventData } = createEventDto;
|
||||||
|
|
||||||
|
const event = this.eventRepository.create(eventData);
|
||||||
|
const savedEvent = await this.eventRepository.save(event);
|
||||||
|
|
||||||
|
if (event.isRecurring && recurrenceRule) {
|
||||||
|
const rule = this.recurrenceRuleRepository.create({
|
||||||
|
...recurrenceRule,
|
||||||
|
event: savedEvent,
|
||||||
|
});
|
||||||
|
await this.recurrenceRuleRepository.save(rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getEventById(savedEvent.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createException(
|
||||||
|
eventId: number,
|
||||||
|
createExceptionDto: CreateExceptionDto,
|
||||||
|
): Promise<EventException> {
|
||||||
|
const event = await this.eventRepository.findOneBy({ id: eventId });
|
||||||
|
if (!event) {
|
||||||
|
throw new NotFoundException(`Event with ID ${eventId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const exception = this.eventExceptionRepository.create({
|
||||||
|
...createExceptionDto,
|
||||||
|
event: event,
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.eventExceptionRepository.save(exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEventById(id: number): Promise<Event> {
|
||||||
|
const event = await this.eventRepository.findOne({
|
||||||
|
where: { id },
|
||||||
|
relations: ['recurrenceRule', 'exceptions', 'eventType'],
|
||||||
|
});
|
||||||
|
if (!event) {
|
||||||
|
throw new NotFoundException(`Event with ID ${id} not found`);
|
||||||
|
}
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateEvent(
|
||||||
|
id: number,
|
||||||
|
updateEventDto: CreateEventDto,
|
||||||
|
): Promise<Event> {
|
||||||
|
await this.eventRepository.update(id, updateEventDto);
|
||||||
|
return this.getEventById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteEvent(id: number): Promise<void> {
|
||||||
|
const result = await this.eventRepository.delete(id);
|
||||||
|
if (result.affected === 0) {
|
||||||
|
throw new NotFoundException(`Event with ID ${id} not found.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
74
server/src/calendar/dto/create-event.dto.ts
Normal file
74
server/src/calendar/dto/create-event.dto.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import {
|
||||||
|
IsString,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsBoolean,
|
||||||
|
IsDate,
|
||||||
|
ValidateNested,
|
||||||
|
IsInt,
|
||||||
|
IsIn,
|
||||||
|
Min,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
|
||||||
|
// DTO for the recurrence rule part
|
||||||
|
export class RecurrenceRuleDto {
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsIn(['DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY'])
|
||||||
|
frequency: 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'YEARLY';
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
interval: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
byDay?: string; // e.g., 'MO,TU,WE,TH,FR'
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsDate()
|
||||||
|
@Type(() => Date)
|
||||||
|
endDate?: Date;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
count?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CreateEventDto {
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsDate()
|
||||||
|
@Type(() => Date)
|
||||||
|
startTime: Date;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsDate()
|
||||||
|
@Type(() => Date)
|
||||||
|
endTime: Date;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
timezone: string; // e.g., 'Europe/Berlin'
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
eventTypeId?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
isRecurring?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => RecurrenceRuleDto)
|
||||||
|
recurrenceRule?: RecurrenceRuleDto;
|
||||||
|
}
|
||||||
31
server/src/calendar/dto/create-exception.dto.ts
Normal file
31
server/src/calendar/dto/create-exception.dto.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { IsDate, IsNotEmpty, IsOptional, IsString, IsBoolean } from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
|
||||||
|
export class CreateExceptionDto {
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsDate()
|
||||||
|
@Type(() => Date)
|
||||||
|
originalStartTime: Date; // The start time of the instance to modify/cancel
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
isCancelled?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsDate()
|
||||||
|
@Type(() => Date)
|
||||||
|
newStartTime?: Date;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsDate()
|
||||||
|
@Type(() => Date)
|
||||||
|
newEndTime?: Date;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
title?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
14
server/src/calendar/dto/get-calendar.dto.ts
Normal file
14
server/src/calendar/dto/get-calendar.dto.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { IsDate, IsNotEmpty } from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
|
||||||
|
export class GetCalendarDto {
|
||||||
|
@IsNotEmpty()
|
||||||
|
@Type(() => Date)
|
||||||
|
@IsDate()
|
||||||
|
startDate: Date;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsDate()
|
||||||
|
@Type(() => Date)
|
||||||
|
endDate: Date;
|
||||||
|
}
|
||||||
@ -6,6 +6,11 @@ import * as dotenv from 'dotenv';
|
|||||||
import { UserGroup } from './entity/user-group';
|
import { UserGroup } from './entity/user-group';
|
||||||
import { UserRole } from './entity/user-role';
|
import { UserRole } from './entity/user-role';
|
||||||
import { EventType } from './entity/event-type.entity';
|
import { EventType } from './entity/event-type.entity';
|
||||||
|
import { Event } from './entity/event.entity';
|
||||||
|
import { EventException } from './entity/event-exception.entity';
|
||||||
|
import { RecurrenceRule } from './entity/recurrence-rule.entity';
|
||||||
|
import { Product } from './entity/product.entity';
|
||||||
|
import { Booking } from './entity/booking.entity';
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
@ -18,9 +23,17 @@ export const AppDataSource = new DataSource({
|
|||||||
database: process.env.DATABASE_NAME,
|
database: process.env.DATABASE_NAME,
|
||||||
synchronize: false,
|
synchronize: false,
|
||||||
logging: false,
|
logging: false,
|
||||||
entities: [User, UserGroup, UserRole, EventType],
|
entities: [
|
||||||
migrations: [
|
User,
|
||||||
'src/migration/**/*.ts'
|
UserGroup,
|
||||||
|
UserRole,
|
||||||
|
EventType,
|
||||||
|
Event,
|
||||||
|
EventException,
|
||||||
|
RecurrenceRule,
|
||||||
|
Product,
|
||||||
|
Booking
|
||||||
],
|
],
|
||||||
|
migrations: ['src/migration/**/*.ts'],
|
||||||
subscribers: [],
|
subscribers: [],
|
||||||
});
|
});
|
||||||
|
|||||||
68
server/src/entity/booking.entity.ts
Normal file
68
server/src/entity/booking.entity.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import {
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Entity,
|
||||||
|
JoinColumn,
|
||||||
|
ManyToOne,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
Index,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { Event } from './event.entity';
|
||||||
|
import { User } from './user';
|
||||||
|
|
||||||
|
@Entity('bookings')
|
||||||
|
@Index(['eventId', 'occurrenceStartTime']) // Index for fast lookups
|
||||||
|
export class Booking {
|
||||||
|
@PrimaryGeneratedColumn({ type: 'bigint' })
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column({ name: 'occurrence_start_time', type: 'timestamptz' })
|
||||||
|
occurrenceStartTime: Date;
|
||||||
|
|
||||||
|
@Column({ name: 'notes', type: 'text', nullable: true })
|
||||||
|
notes: string;
|
||||||
|
|
||||||
|
@Column({ name: 'reserved_seats_count', type: 'integer', default: 1 })
|
||||||
|
reservedSeatsCount: number;
|
||||||
|
|
||||||
|
@CreateDateColumn({ name: 'created_at', type: 'timestamp' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp' })
|
||||||
|
updatedAt: Date;
|
||||||
|
|
||||||
|
// --- Cancellation Fields ---
|
||||||
|
@Column({ name: 'canceled_at', type: 'timestamp', nullable: true })
|
||||||
|
canceledAt: Date | null;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'canceled_reason',
|
||||||
|
type: 'varchar',
|
||||||
|
length: 50,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
canceledReason: string | null;
|
||||||
|
|
||||||
|
// --- Relationships ---
|
||||||
|
@Column({ name: 'event_id', type: 'bigint', nullable: true })
|
||||||
|
eventId: number | null;
|
||||||
|
|
||||||
|
@Column({ name: 'user_id', type: 'bigint', nullable: true })
|
||||||
|
userId: number | null;
|
||||||
|
|
||||||
|
@Column({ name: 'canceled_by_user_id', type: 'bigint', nullable: true })
|
||||||
|
canceledByUserId: number | null;
|
||||||
|
|
||||||
|
@ManyToOne(() => Event, { onDelete: 'SET NULL', nullable: true })
|
||||||
|
@JoinColumn({ name: 'event_id' })
|
||||||
|
event: Event;
|
||||||
|
|
||||||
|
@ManyToOne(() => User, { onDelete: 'SET NULL', nullable: true })
|
||||||
|
@JoinColumn({ name: 'user_id' })
|
||||||
|
user: User;
|
||||||
|
|
||||||
|
@ManyToOne(() => User, { onDelete: 'SET NULL', nullable: true })
|
||||||
|
@JoinColumn({ name: 'canceled_by_user_id' })
|
||||||
|
canceledByUser: User;
|
||||||
|
}
|
||||||
@ -1,52 +1,47 @@
|
|||||||
// dvbooking-cli/src/templates/nestjs/entity.ts.tpl
|
|
||||||
|
|
||||||
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
|
||||||
import {
|
import {
|
||||||
IsString,
|
Column,
|
||||||
IsNumber,
|
CreateDateColumn,
|
||||||
IsBoolean,
|
Entity,
|
||||||
IsDate,
|
JoinColumn,
|
||||||
IsOptional,
|
ManyToOne,
|
||||||
} from 'class-validator';
|
PrimaryGeneratedColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { Event } from './event.entity';
|
||||||
|
|
||||||
@Entity({ name: 'event_exceptions' })
|
@Entity('event_exceptions')
|
||||||
export class EventException {
|
export class EventException {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn({ type: 'bigint' })
|
||||||
id: number;
|
id: number;
|
||||||
|
|
||||||
@Column()
|
@Column({ name: 'original_start_time', type: 'timestamptz' })
|
||||||
@IsNumber()
|
originalStartTime: Date;
|
||||||
event_id: number;
|
|
||||||
|
|
||||||
@Column()
|
@Column({ name: 'is_cancelled', type: 'boolean', default: false })
|
||||||
@IsDate()
|
isCancelled: boolean;
|
||||||
original_start_time: Date;
|
|
||||||
|
|
||||||
@Column({ default: false })
|
@Column({ name: 'new_start_time', type: 'timestamptz', nullable: true })
|
||||||
@IsBoolean()
|
newStartTime: Date;
|
||||||
is_cancelled: boolean = false;
|
|
||||||
|
|
||||||
@Column({ type: 'timestamp with time zone', nullable: true })
|
@Column({ name: 'new_end_time', type: 'timestamptz', nullable: true })
|
||||||
@IsOptional()
|
newEndTime: Date;
|
||||||
@IsDate()
|
|
||||||
new_start_time: Date | null;
|
|
||||||
|
|
||||||
@Column({ type: 'timestamp with time zone', nullable: true })
|
@Column({ name: 'title', type: 'varchar', length: 255, nullable: true })
|
||||||
@IsOptional()
|
title: string;
|
||||||
@IsDate()
|
|
||||||
new_end_time: Date | null;
|
|
||||||
|
|
||||||
@Column({ type: 'character varying', nullable: true })
|
@Column({ name: 'description', type: 'text', nullable: true })
|
||||||
@IsOptional()
|
description: string;
|
||||||
@IsString()
|
|
||||||
title: string | null;
|
|
||||||
|
|
||||||
@Column({ type: 'text', nullable: true })
|
@CreateDateColumn({ name: 'created_at', type: 'timestamp' })
|
||||||
@IsOptional()
|
createdAt: Date;
|
||||||
@IsString()
|
|
||||||
description: string | null;
|
|
||||||
|
|
||||||
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
// --- Relationships ---
|
||||||
@IsDate()
|
|
||||||
created_at: Date;
|
@Column({ name: 'event_id', type: 'bigint' })
|
||||||
|
eventId: number;
|
||||||
|
|
||||||
|
@ManyToOne(() => Event, (event) => event.exceptions, {
|
||||||
|
onDelete: 'CASCADE', // If the parent event is deleted, delete its exceptions
|
||||||
|
})
|
||||||
|
@JoinColumn({ name: 'event_id' })
|
||||||
|
event: Event;
|
||||||
}
|
}
|
||||||
@ -1,10 +1,8 @@
|
|||||||
// dvbooking-cli/src/templates/nestjs/entity.ts.tpl
|
// dvbooking-cli/src/templates/nestjs/entity.ts.tpl
|
||||||
|
|
||||||
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
|
||||||
import {
|
import { Event } from './event.entity';
|
||||||
IsString,
|
import { IsString, IsOptional } from 'class-validator';
|
||||||
IsOptional,
|
|
||||||
} from 'class-validator';
|
|
||||||
|
|
||||||
@Entity({ name: 'event_type' })
|
@Entity({ name: 'event_type' })
|
||||||
export class EventType {
|
export class EventType {
|
||||||
@ -24,4 +22,7 @@ export class EventType {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
color: string | null;
|
color: string | null;
|
||||||
|
|
||||||
|
@OneToMany(() => Event, (event) => event.eventType)
|
||||||
|
events: Event[];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,54 +1,66 @@
|
|||||||
// dvbooking-cli/src/templates/nestjs/entity.ts.tpl
|
|
||||||
|
|
||||||
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
|
||||||
import {
|
import {
|
||||||
IsString,
|
Column,
|
||||||
IsNumber,
|
CreateDateColumn,
|
||||||
IsBoolean,
|
Entity,
|
||||||
IsDate,
|
ManyToOne,
|
||||||
IsOptional,
|
OneToMany,
|
||||||
} from 'class-validator';
|
OneToOne,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
JoinColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { EventType } from './event-type.entity';
|
||||||
|
import { RecurrenceRule } from './recurrence-rule.entity';
|
||||||
|
import { EventException } from './event-exception.entity';
|
||||||
|
|
||||||
@Entity({ name: 'events' })
|
@Entity('events')
|
||||||
export class Event {
|
export class Event {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn({ type: 'bigint' })
|
||||||
id: number;
|
id: number;
|
||||||
|
|
||||||
@Column({ type: 'bigint', nullable: true })
|
@Column({ name: 'title', type: 'varchar', length: 255 })
|
||||||
@IsOptional()
|
|
||||||
@IsNumber()
|
|
||||||
event_type_id: number | null;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
@IsString()
|
|
||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
@Column({ type: 'text', nullable: true })
|
@Column({ name: 'description', type: 'text', nullable: true })
|
||||||
@IsOptional()
|
description: string;
|
||||||
@IsString()
|
|
||||||
description: string | null;
|
|
||||||
|
|
||||||
@Column()
|
@Column({ name: 'start_time', type: 'timestamptz' })
|
||||||
@IsDate()
|
startTime: Date;
|
||||||
start_time: Date;
|
|
||||||
|
|
||||||
@Column()
|
@Column({ name: 'end_time', type: 'timestamptz' })
|
||||||
@IsDate()
|
endTime: Date;
|
||||||
end_time: Date;
|
|
||||||
|
|
||||||
@Column()
|
@Column({ name: 'timezone', type: 'varchar', length: 50 })
|
||||||
@IsString()
|
|
||||||
timezone: string;
|
timezone: string;
|
||||||
|
|
||||||
@Column({ default: false })
|
@Column({ name: 'is_recurring', type: 'boolean', default: false })
|
||||||
@IsBoolean()
|
isRecurring: boolean;
|
||||||
is_recurring: boolean = false;
|
|
||||||
|
|
||||||
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
@CreateDateColumn({ name: 'created_at', type: 'timestamp' })
|
||||||
@IsDate()
|
createdAt: Date;
|
||||||
created_at: Date;
|
|
||||||
|
|
||||||
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp' })
|
||||||
@IsDate()
|
updatedAt: Date;
|
||||||
updated_at: Date;
|
|
||||||
|
// --- Relationships ---
|
||||||
|
|
||||||
|
@Column({ name: 'event_type_id', type: 'bigint', nullable: true })
|
||||||
|
eventTypeId: number;
|
||||||
|
|
||||||
|
@ManyToOne(() => EventType, (eventType) => eventType.events, {
|
||||||
|
nullable: true,
|
||||||
|
onDelete: 'SET NULL', // As requested for optional relationship
|
||||||
|
})
|
||||||
|
@JoinColumn({ name: 'event_type_id' })
|
||||||
|
eventType: EventType;
|
||||||
|
|
||||||
|
@OneToOne(() => RecurrenceRule, (rule) => rule.event, {
|
||||||
|
cascade: true, // Automatically save/update recurrence rule when event is saved
|
||||||
|
})
|
||||||
|
recurrenceRule: RecurrenceRule;
|
||||||
|
|
||||||
|
@OneToMany(() => EventException, (exception) => exception.event, {
|
||||||
|
cascade: true, // Automatically save/update exceptions when event is saved
|
||||||
|
})
|
||||||
|
exceptions: EventException[];
|
||||||
}
|
}
|
||||||
@ -1,47 +1,46 @@
|
|||||||
// dvbooking-cli/src/templates/nestjs/entity.ts.tpl
|
import {
|
||||||
|
Column,
|
||||||
|
Entity,
|
||||||
|
JoinColumn,
|
||||||
|
OneToOne,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { Event } from './event.entity';
|
||||||
|
|
||||||
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
@Entity('recurrence_rules')
|
||||||
import { IsString, IsNumber, IsDate, IsOptional } from 'class-validator';
|
|
||||||
|
|
||||||
@Entity({ name: 'recurrence_rules' })
|
|
||||||
export class RecurrenceRule {
|
export class RecurrenceRule {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn({ type: 'bigint' })
|
||||||
id: number;
|
id: number;
|
||||||
|
|
||||||
@Column()
|
@Column({ name: 'frequency', type: 'varchar', length: 10 })
|
||||||
@IsNumber()
|
frequency: 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'YEARLY';
|
||||||
event_id: number;
|
|
||||||
|
|
||||||
@Column()
|
@Column({ name: 'interval', type: 'integer', default: 1 })
|
||||||
@IsString()
|
interval: number;
|
||||||
frequency: string;
|
|
||||||
|
|
||||||
@Column({ default: '1' })
|
@Column({ name: 'end_date', type: 'date', nullable: true })
|
||||||
@IsNumber()
|
endDate: Date;
|
||||||
interval: number = 1;
|
|
||||||
|
|
||||||
@Column({ type: 'date', nullable: true })
|
@Column({ name: 'count', type: 'integer', nullable: true })
|
||||||
@IsOptional()
|
count: number;
|
||||||
@IsDate()
|
|
||||||
end_date: Date | null;
|
|
||||||
|
|
||||||
@Column({ type: 'integer', nullable: true })
|
@Column({ name: 'by_day', type: 'varchar', length: 20, nullable: true })
|
||||||
@IsOptional()
|
byDay: string; // e.g., 'MO,TU,WE'
|
||||||
@IsNumber()
|
|
||||||
count: number | null;
|
|
||||||
|
|
||||||
@Column({ type: 'character varying', nullable: true })
|
@Column({ name: 'by_month_day', type: 'integer', nullable: true })
|
||||||
@IsOptional()
|
byMonthDay: number;
|
||||||
@IsString()
|
|
||||||
by_day: string | null;
|
|
||||||
|
|
||||||
@Column({ type: 'integer', nullable: true })
|
@Column({ name: 'by_month', type: 'integer', nullable: true })
|
||||||
@IsOptional()
|
byMonth: number;
|
||||||
@IsNumber()
|
|
||||||
by_month_day: number | null;
|
|
||||||
|
|
||||||
@Column({ type: 'integer', nullable: true })
|
// --- Relationships ---
|
||||||
@IsOptional()
|
|
||||||
@IsNumber()
|
@Column({ name: 'event_id', type: 'bigint' })
|
||||||
by_month: number | null;
|
eventId: number;
|
||||||
|
|
||||||
|
@OneToOne(() => Event, (event) => event.recurrenceRule, {
|
||||||
|
onDelete: 'CASCADE', // If the parent event is deleted, delete the rule too
|
||||||
|
})
|
||||||
|
@JoinColumn({ name: 'event_id' })
|
||||||
|
event: Event;
|
||||||
}
|
}
|
||||||
|
|||||||
31
server/src/migration/1763106308123-add_booking_table.ts
Normal file
31
server/src/migration/1763106308123-add_booking_table.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddBookingTable1763106308123 implements MigrationInterface {
|
||||||
|
name = 'AddBookingTable1763106308123';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE bookings (
|
||||||
|
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||||
|
event_id BIGINT,
|
||||||
|
occurrence_start_time TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
user_id BIGINT,
|
||||||
|
notes TEXT,
|
||||||
|
reserved_seats_count INTEGER NOT NULL default 1,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
canceled_at TIMESTAMP NULL,
|
||||||
|
canceled_reason varchar(50) NULL,
|
||||||
|
canceled_by_user_id BIGINT NULL,
|
||||||
|
constraint fk_booking_event FOREIGN KEY (event_id) REFERENCES events (id) ON DELETE SET NULL,
|
||||||
|
constraint fk_booking_user FOREIGN KEY (user_id) REFERENCES "user" (id) ON DELETE SET NULL,
|
||||||
|
constraint fk_booking_cancel_user FOREIGN KEY (canceled_by_user_id) REFERENCES "user" (id) ON DELETE SET NULL
|
||||||
|
);`,
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`DROP TABLE "bookings"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -22,10 +22,7 @@ export class RecurrenceRulesService {
|
|||||||
private readonly recurrenceRuleRepository: Repository<RecurrenceRule>,
|
private readonly recurrenceRuleRepository: Repository<RecurrenceRule>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private readonly searchableFields: (keyof RecurrenceRule)[] = [
|
private readonly searchableFields: (keyof RecurrenceRule)[] = [ ];
|
||||||
'frequency',
|
|
||||||
'by_day',
|
|
||||||
];
|
|
||||||
|
|
||||||
create(createRecurrenceRuleDto: CreateRecurrenceRuleDto) {
|
create(createRecurrenceRuleDto: CreateRecurrenceRuleDto) {
|
||||||
const newRecord = this.recurrenceRuleRepository.create(
|
const newRecord = this.recurrenceRuleRepository.create(
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user