diff --git a/api/src/availability/availability.controller.ts b/api/src/availability/availability.controller.ts new file mode 100644 index 0000000..e11bb88 --- /dev/null +++ b/api/src/availability/availability.controller.ts @@ -0,0 +1,118 @@ +import { Body, Controller, Delete, Param, Patch, Post } from '@nestjs/common'; +import { AvailabilityService } from './availability.service'; +import { + Action, + insertAvailabilitySchema, + updateAvailabilitySchema, +} from '@hkrecruitment/shared'; +import { JoiValidate } from '../joi-validation/joi-validate.decorator'; +import { + ApiBadRequestResponse, + ApiBearerAuth, + ApiForbiddenResponse, + ApiNotFoundResponse, + ApiCreatedResponse, + ApiOkResponse, + ApiTags, + ApiConflictResponse, + ApiNoContentResponse, + ApiBadGatewayResponse, +} from '@nestjs/swagger'; +import { CheckPolicies } from 'src/authorization/check-policies.decorator'; +import { Availability } from './availability.entity'; +import Joi from 'joi'; + +@ApiBearerAuth() +@ApiTags('timeslots') +@Controller('timeslots') +export class AvailabilityController { + constructor(private readonly availabilityService: AvailabilityService) {} + + @ApiBadRequestResponse() + @ApiForbiddenResponse() + @ApiNotFoundResponse() + @ApiOkResponse() + @ApiNoContentResponse() + @ApiBadGatewayResponse() + async listAvailabilities(): Promise { + const matches = await this.availabilityService.listAvailabilities(); + return matches; + } + + @ApiBadRequestResponse() + @ApiForbiddenResponse() + @ApiNotFoundResponse() + @ApiOkResponse() + @ApiNoContentResponse() + @ApiBadGatewayResponse() + @JoiValidate({ + param: Joi.number().positive().integer().required(), + }) + async findAvailabilityById(id: number): Promise { + const matches = await this.availabilityService.findAvailabilityById(id); + return matches; + } + + @ApiBadRequestResponse() + @ApiForbiddenResponse() + @ApiNotFoundResponse() + @ApiOkResponse() + @ApiNoContentResponse() + @ApiBadGatewayResponse() + @ApiConflictResponse() + @ApiCreatedResponse() + @CheckPolicies((ability) => ability.can(Action.Create, 'Availability')) + @Post() + @JoiValidate({ + body: insertAvailabilitySchema, + }) + async createAvailability(@Body() body: Availability): Promise { + const res = await this.availabilityService.createAvailability(body); + if (res.identifiers.length > 0) { + return true; + } + return false; + } + + @ApiBadRequestResponse() + @ApiForbiddenResponse() + @ApiNotFoundResponse() + @ApiOkResponse() + @ApiNoContentResponse() + @ApiBadGatewayResponse() + @ApiConflictResponse() + @ApiCreatedResponse() + @CheckPolicies((ability) => ability.can(Action.Create, 'Availability')) + @Patch() + @JoiValidate({ + body: updateAvailabilitySchema, + }) + async updateAvailability(@Body() body: Availability): Promise { + const res = await this.availabilityService.updateAvailability(body); + if (res.affected != undefined && res.affected != null && res.affected > 0) { + return true; + } + return false; + } + + @ApiBadRequestResponse() + @ApiForbiddenResponse() + @ApiNotFoundResponse() + @ApiOkResponse() + @ApiNoContentResponse() + @ApiBadGatewayResponse() + @ApiConflictResponse() + @ApiCreatedResponse() + @CheckPolicies((ability) => ability.can(Action.Create, 'Availability')) + @Delete() + @JoiValidate({ + param: Joi.number().positive().integer().required(), + }) + async deleteAvailability(@Param() id: number): Promise { + const res = await this.availabilityService.deleteAvailability(id); + if (res.affected != undefined && res.affected != null && res.affected > 0) { + return true; + } + return false; + } +} diff --git a/api/src/availability/availability.entity.ts b/api/src/availability/availability.entity.ts new file mode 100644 index 0000000..97188d0 --- /dev/null +++ b/api/src/availability/availability.entity.ts @@ -0,0 +1,28 @@ +import { + Column, + Entity, + JoinColumn, + OneToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { + Availability as AvailabilityInterface, + AvailabilityState, +} from '@hkrecruitment/shared/availability'; +import { User } from 'src/users/user.entity'; +import { TimeSlot } from 'src/timeslots/timeslot.entity'; + +@Entity() +export class Availability implements AvailabilityInterface { + @PrimaryGeneratedColumn('increment') + id: number; + @Column() + state: AvailabilityState; + @Column() + lastModified: Date; + @OneToOne(() => TimeSlot) + timeSlot: TimeSlot; + @OneToOne(() => User) + @JoinColumn() + user: User; +} diff --git a/api/src/availability/availability.module.ts b/api/src/availability/availability.module.ts new file mode 100644 index 0000000..10d80fb --- /dev/null +++ b/api/src/availability/availability.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { AvailabilityService } from './availability.service'; +import { AvailabilityController } from './availability.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Availability } from './availability.entity'; +import { UsersModule } from 'src/users/users.module'; + +@Module({ + imports: [TypeOrmModule.forFeature([Availability]), UsersModule], + providers: [AvailabilityService], + controllers: [AvailabilityController], + exports: [AvailabilityService], +}) +export class AvailabilityModule {} diff --git a/api/src/availability/availability.service.ts b/api/src/availability/availability.service.ts new file mode 100644 index 0000000..7f7a303 --- /dev/null +++ b/api/src/availability/availability.service.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { DeleteResult, InsertResult, Repository, UpdateResult } from 'typeorm'; +import { Availability } from './availability.entity'; + +@Injectable() +export class AvailabilityService { + constructor( + @InjectRepository(Availability) + private readonly availabilityRepository: Repository, + ) {} + + async listAvailabilities(): Promise { + return await this.availabilityRepository.find(); + } + + async findAvailabilityById(id: number): Promise { + const matches = await this.availabilityRepository.findBy({ + id: id, + }); + return matches.length > 0 ? matches[0] : null; + } + + async createAvailability(availability: Availability): Promise { + return await this.availabilityRepository.insert(availability); + } + + async updateAvailability(availability: Availability): Promise { + return await this.availabilityRepository.update( + availability.id, + availability, + ); + } + + async deleteAvailability(id: number): Promise { + return await this.availabilityRepository.delete(id); + } +} diff --git a/shared/src/availability.ts b/shared/src/availability.ts index 1cd0e38..ecdbd38 100644 --- a/shared/src/availability.ts +++ b/shared/src/availability.ts @@ -1,5 +1,6 @@ +import { TimeSlot, createTimeSlotSchema } from "timeslot"; import { Action, ApplyAbilities } from "./abilities"; -import { Person, Role } from "./person"; +import { Person, Role, createUserSchema } from "./person"; import * as Joi from "joi"; export enum AvailabilityState { @@ -14,21 +15,33 @@ export enum AvailabilityType { } export interface Availability { + id: number; state: AvailabilityState; - timeSlotId: number; - member: Person; - // assignedAt?: Date; - // confirmedAt?: Date; - // cancelledAt?: Date; + lastModified: Date; + timeSlot: TimeSlot; + user: Person; } /* Validation schemas */ +export const insertAvailabilitySchema = Joi.object({ + state: Joi.string() + .valid(...Object.values(AvailabilityType)) + .required(), + lastModified: Joi.date().required(), + timeSlot: createTimeSlotSchema.required(), + user: createUserSchema.required(), +}).options({ + stripUnknown: true, + abortEarly: false, + presence: "required", +}); + export const updateAvailabilitySchema = Joi.object({ + id: Joi.number().positive().integer().required(), state: Joi.string() .valid(...Object.values(AvailabilityType)) .required(), - timeSlotId: Joi.number().positive().required(), }).options({ stripUnknown: true, abortEarly: false, diff --git a/shared/src/timeslot.ts b/shared/src/timeslot.ts index fea1951..712b603 100644 --- a/shared/src/timeslot.ts +++ b/shared/src/timeslot.ts @@ -1,5 +1,4 @@ import { Action, ApplyAbilities } from "./abilities"; -import { Role } from "./person"; import DateExtension from "@joi/date"; import * as Joi from "joi"; const JoiDate = Joi.extend(DateExtension);