diff --git a/src/formResult/dto/create-formResult.dto.ts b/src/formResult/dto/create-formResult.dto.ts new file mode 100644 index 0000000..b5b0263 --- /dev/null +++ b/src/formResult/dto/create-formResult.dto.ts @@ -0,0 +1,42 @@ +import { IsNumber, IsString, IsOptional, IsUUID, ValidateNested, IsArray, IsDateString } from 'class-validator'; +import { Type } from 'class-transformer'; +import { BaseDto } from '../../_core/dto/base.dto'; + +// DTO for Options +class CreateOptionsDto { + @IsString() + value: string; + + @IsNumber() + count: number; +} + +// DTO for Opinion +class CreateOpinionDto { + @ValidateNested() + @Type(() => CreateOptionsDto) + options: CreateOptionsDto; + + @IsUUID() + questionId: string; +} + +// Main DTO for creating a FormResult +export class CreateFormResultDto extends BaseDto { + @IsNumber() + numParticipants: number; + + @IsNumber() + numQuestions: number; + + @IsNumber() + numAnswers: number; + + @IsNumber() + numComplete: number; + + @IsArray() + @ValidateNested({ each: true }) + @Type(() => CreateOpinionDto) + opinions: CreateOpinionDto[]; +} \ No newline at end of file diff --git a/src/formResult/dto/update-formResult.dto.ts b/src/formResult/dto/update-formResult.dto.ts new file mode 100644 index 0000000..1474008 --- /dev/null +++ b/src/formResult/dto/update-formResult.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateFormResultDto } from './create-formResult.dto'; + +export class UpdateFormResultDto extends PartialType(CreateFormResultDto) {} diff --git a/src/formResult/entity/formResult.entity.ts b/src/formResult/entity/formResult.entity.ts new file mode 100644 index 0000000..f0385e7 --- /dev/null +++ b/src/formResult/entity/formResult.entity.ts @@ -0,0 +1,98 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { Document } from 'mongoose'; +import { BaseEntity } from 'src/_core/entity/_base.entity'; +import * as mongoosePaginate from 'mongoose-paginate-v2'; +import { Attachment, AttachmentSchema } from '../../attachment/entity/attachment.entity'; +import { Option, OptionSchema } from '../../option/entity/option.entity'; +import { UUID } from 'mongodb'; + +// Sub-schema for Options +@Schema({ _id: false, id: false }) +class Options { + @Prop({ + type: String, + required: true, + }) + value: string; + + @Prop({ + type: Number, + required: true, + min: 0, + default: 0 + }) + count: number; +} + +export const OptionsSchema = SchemaFactory.createForClass(Options); + +// Sub-schema for Opinion +@Schema({ _id: false, id: false }) +class Opinion { + @Prop({ + type: OptionsSchema, + required: true, + }) + options: Options; + + @Prop({ + type: UUID, + required: true, + }) + questionId: string; +} +export const OpinionSchema = SchemaFactory.createForClass(Opinion); + + +@Schema({ _id: false, id: false }) // Disable _id and virtual id +export class FormResult extends BaseEntity { + @Prop({ + type: Number, + required: true, + min: 0, + default: 0, + }) + numParticipants: string; + + @Prop({ + type: Number, + required: true, + min: 0, + default: 0, + }) + numQuestions: string; + + @Prop({ + type: Number, + required: true, + min: 0, + default: 0, + }) + numAnswers: string; + + @Prop({ + type: Number, + required: true, + min: 0, + default: 0, + }) + numComplete: string; // number of people who completed this form + + @Prop({ + type: [OpinionSchema], + default: [], + }) + opinions: Opinion[]; +} + +export type FormResultDocument = FormResult & Document; +export const FormResultSchema = SchemaFactory.createForClass(FormResult); +FormResultSchema.plugin(mongoosePaginate); + +// Transform the output to remove the internal '_id' +FormResultSchema.set('toJSON', { + transform: (doc: FormResultDocument, ret: FormResult & { _id?: any }) => { + delete ret._id; + return ret; + }, +}); diff --git a/src/formResult/formResult.controller.ts b/src/formResult/formResult.controller.ts new file mode 100644 index 0000000..359b7d3 --- /dev/null +++ b/src/formResult/formResult.controller.ts @@ -0,0 +1,47 @@ +import { Controller, Get, Post, Patch, Delete, Param, Body, UsePipes, ValidationPipe, Query, ParseUUIDPipe, HttpCode, HttpStatus,} from '@nestjs/common'; +import { FormResultService } from './formResult.service'; +import { CreateFormResultDto } from './dto/create-formResult.dto'; +import { UpdateFormResultDto } from './dto/update-formResult.dto'; +import { FormResult } from './entity/formResult.entity'; + +@Controller('formResults') +export class FormResultController { + constructor(private readonly formResultService: FormResultService) {} + + @Post() + @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) + async create(@Body() body: CreateFormResultDto): Promise { + return this.formResultService.create(body); + } + + @Get('findAll') + async findAll( + @Query('page') page: string = '1', + @Query('limit') limit: string = '10', + ) { + // The service returns the full pagination object + return this.formResultService.findAll(parseInt(page, 10), parseInt(limit, 10)); + } + + @Get(':id') + async findOne( + @Param('id', new ParseUUIDPipe()) id: string, + ): Promise { + return this.formResultService.findById(id); + } + + @Patch(':id') + @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) + async update( + @Param('id', new ParseUUIDPipe()) id: string, + @Body() body: UpdateFormResultDto, + ): Promise { + return this.formResultService.update(id, body); + } + + @Delete(':id') + @HttpCode(HttpStatus.NO_CONTENT) + async remove(@Param('id', new ParseUUIDPipe()) id: string): Promise { + return this.formResultService.remove(id); + } +} diff --git a/src/formResult/formResult.module.ts b/src/formResult/formResult.module.ts new file mode 100644 index 0000000..0f12280 --- /dev/null +++ b/src/formResult/formResult.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { FormResultService } from './formResult.service'; +import { FormResultController } from './formResult.controller'; +import { FormResult, FormResultSchema } from './entity/formResult.entity'; + +@Module({ + imports: [ + MongooseModule.forFeature([ + { name: FormResult.name, schema: FormResultSchema }, + ]), + ], + controllers: [FormResultController], + providers: [FormResultService], +}) +export class FormResultModule {} diff --git a/src/formResult/formResult.service.ts b/src/formResult/formResult.service.ts new file mode 100644 index 0000000..c0a98ad --- /dev/null +++ b/src/formResult/formResult.service.ts @@ -0,0 +1,82 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; +import { FormResult, FormResultDocument } from './entity/formResult.entity'; +import { CreateFormResultDto } from './dto/create-formResult.dto'; +import { UpdateFormResultDto } from './dto/update-formResult.dto'; +import { PaginateModel } from '../participant/participant.service'; + +@Injectable() +export class FormResultService { + constructor( + @InjectModel(FormResult.name) + private readonly formResultModel: PaginateModel, + ) {} + + async create(data: CreateFormResultDto): Promise { + const formResult = new this.formResultModel({ + ...data, + id: data.id || undefined, // Let BaseEntity generate UUID if not provided + metadata: data.metadata || { entries: [] }, + }); + return formResult.save(); + } + + async findAll( + page = 1, + limit = 10, + ): Promise<{ + docs: FormResult[]; + totalDocs: number; + limit: number; + page: number; + totalPages: number; + hasNextPage: boolean; + hasPrevPage: boolean; + nextPage: number | null; + prevPage: number | null; + }> { + // Selects all fields from the FormResult entity for the response + return this.formResultModel.paginate( + {}, + { page, limit, lean: true }, + ); + } + + async findById(id: string): Promise { + const formResult = await this.formResultModel + .findOne({ id }) + .lean() + .exec(); + if (!formResult) { + throw new NotFoundException(`FormResult with ID "${id}" not found`); + } + return formResult; + } + + async update( + id: string, + data: UpdateFormResultDto, + ): Promise { + const updatedFormResult = await this.formResultModel + .findOneAndUpdate( + { id }, + { $set: { ...data, updatedAt: new Date() } }, + { new: true }, + ) + .lean() + .exec(); + + if (!updatedFormResult) { + throw new NotFoundException(`FormResult with ID "${id}" not found`); + } + return updatedFormResult; + } + + async remove(id: string): Promise { + const result = await this.formResultModel.deleteOne({ id }).exec(); + if (result.deletedCount === 0) { + throw new NotFoundException(`FormResult with ID "${id}" not found`); + } + } +} diff --git a/src/formSection/dto/create-formSection.dto.ts b/src/formSection/dto/create-formSection.dto.ts new file mode 100644 index 0000000..06c65db --- /dev/null +++ b/src/formSection/dto/create-formSection.dto.ts @@ -0,0 +1,43 @@ +import { IsString, IsOptional, IsUUID, ValidateNested, IsArray, IsEnum, IsDateString } from 'class-validator'; +import { Type } from 'class-transformer'; +import { BaseDto } from '../../_core/dto/base.dto'; +import { CreateAttachmentDto } from '../../attachment/dto/create-attachment.dto'; +import { CreateQuestionDto } from '../../question/dto/create-question.dto'; + +// DTO for DisplayCondition +export class CreateDisplayConditionDto { + @IsUUID() + answer: string; + + @IsUUID() + question: string; + + @IsEnum(['equal', 'contains', 'not_equal', 'not_contains']) + relation: 'equal' | 'contains' | 'not_equal' | 'not_contains'; +} +//\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\//\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\//\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/ +export class CreateFormSectionDto extends BaseDto { + @IsString() + title: string; + + @IsString() + description: string; + + @IsArray() + @ValidateNested({ each: true }) + @Type(() => CreateAttachmentDto) + @IsOptional() + attachments?: CreateAttachmentDto[]; + + @IsArray() + @ValidateNested({ each: true }) + @Type(() => CreateDisplayConditionDto) + @IsOptional() + displayCondition?: CreateDisplayConditionDto[]; + + @IsArray() + @ValidateNested({ each: true }) + @Type(() => CreateQuestionDto) + @IsOptional() + questions?: CreateQuestionDto[]; +} \ No newline at end of file diff --git a/src/formSection/dto/update-formSection.dto.ts b/src/formSection/dto/update-formSection.dto.ts new file mode 100644 index 0000000..0224d27 --- /dev/null +++ b/src/formSection/dto/update-formSection.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateFormSectionDto } from './create-formSection.dto'; + +export class UpdateFormSectionDto extends PartialType(CreateFormSectionDto) {} diff --git a/src/formSection/entity/formSection.entity.ts b/src/formSection/entity/formSection.entity.ts new file mode 100644 index 0000000..3979607 --- /dev/null +++ b/src/formSection/entity/formSection.entity.ts @@ -0,0 +1,85 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { Document } from 'mongoose'; +import { BaseEntity } from 'src/_core/entity/_base.entity'; +import * as mongoosePaginate from 'mongoose-paginate-v2'; +import { Attachment, AttachmentSchema } from '../../attachment/entity/attachment.entity'; +import { Option, OptionSchema } from '../../option/entity/option.entity'; +import { Answer, AnswerSchema } from '../../answer/entity/answer.entity'; +import { Question, QuestionSchema } from '../../question/entity/question.entity'; + +// Sub-schema for DisplayContition +@Schema({ _id: false, id: false }) +export class DisplayCondition { + @Prop({ + type: [AnswerSchema], + required: true, + }) + answer: Answer; + + @Prop({ + type: [QuestionSchema], + required: true, + }) + question: Question; + + @Prop({ + type: String, + enum: ['equal','contains','not_equal','not_contains'], + required: true, + }) + relation: 'equal'|'contains'|'not_equal'|'not_contains'; +} + +export const DisplayConditionSchema = SchemaFactory.createForClass(DisplayCondition); + + +//\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\ + +@Schema({ _id: false, id: false }) // Disable _id and virtual id +export class FormSection extends BaseEntity { + @Prop({ + type: String, + required: true, + }) + title: string; + + @Prop({ + type: String, + required: true, + }) + description: string; + + @Prop({ + type: [AttachmentSchema], + default: [], + }) + attachments: Attachment[]; + + @Prop({ + type: [DisplayContidionSchema], + default: [], + }) + displayCondition: DisplayContidion[]; + + @Prop({ + type: [QuestionSchema], + default: [], + }) + questions: Question[]; + +} + +export type FormSectionDocument = FormSection & Document; +export const FormSectionSchema = SchemaFactory.createForClass(FormSection); +FormSectionSchema.plugin(mongoosePaginate); + +// Transform the output to remove the internal '_id' +FormSectionSchema.set('toJSON', { + transform: (doc: FormSectionDocument, ret: FormSection & { _id?: any }) => { + delete ret._id; + return ret; + }, +}); + + + diff --git a/src/formSection/formSection.controller.ts b/src/formSection/formSection.controller.ts new file mode 100644 index 0000000..93cc7a3 --- /dev/null +++ b/src/formSection/formSection.controller.ts @@ -0,0 +1,47 @@ +import { Controller, Get, Post, Patch, Delete, Param, Body, UsePipes, ValidationPipe, Query, ParseUUIDPipe, HttpCode, HttpStatus,} from '@nestjs/common'; +import { FormSectionService } from './formSection.service'; +import { CreateFormSectionDto } from './dto/create-formSection.dto'; +import { UpdateFormSectionDto } from './dto/update-formSection.dto'; +import { FormSection } from './entity/formSection.entity'; + +@Controller('formSections') +export class FormSectionController { + constructor(private readonly formSectionService: FormSectionService) {} + + @Post() + @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) + async create(@Body() body: CreateFormSectionDto): Promise { + return this.formSectionService.create(body); + } + + @Get('findAll') + async findAll( + @Query('page') page: string = '1', + @Query('limit') limit: string = '10', + ) { + // The service returns the full pagination object + return this.formSectionService.findAll(parseInt(page, 10), parseInt(limit, 10)); + } + + @Get(':id') + async findOne( + @Param('id', new ParseUUIDPipe()) id: string, + ): Promise { + return this.formSectionService.findById(id); + } + + @Patch(':id') + @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) + async update( + @Param('id', new ParseUUIDPipe()) id: string, + @Body() body: UpdateFormSectionDto, + ): Promise { + return this.formSectionService.update(id, body); + } + + @Delete(':id') + @HttpCode(HttpStatus.NO_CONTENT) + async remove(@Param('id', new ParseUUIDPipe()) id: string): Promise { + return this.formSectionService.remove(id); + } +} diff --git a/src/formSection/formSection.module.ts b/src/formSection/formSection.module.ts new file mode 100644 index 0000000..630fce2 --- /dev/null +++ b/src/formSection/formSection.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { FormSectionService } from './formSection.service'; +import { FormSectionController } from './formSection.controller'; +import { FormSection, FormSectionSchema } from './entity/formSection.entity'; + +@Module({ + imports: [ + MongooseModule.forFeature([ + { name: FormSection.name, schema: FormSectionSchema }, + ]), + ], + controllers: [FormSectionController], + providers: [FormSectionService], +}) +export class FormSectionModule {} diff --git a/src/formSection/formSection.service.ts b/src/formSection/formSection.service.ts new file mode 100644 index 0000000..97e5e3a --- /dev/null +++ b/src/formSection/formSection.service.ts @@ -0,0 +1,82 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; +import { FormSection, FormSectionDocument } from './entity/formSection.entity'; +import { CreateFormSectionDto } from './dto/create-formSection.dto'; +import { UpdateFormSectionDto } from './dto/update-formSection.dto'; +import { PaginateModel } from '../participant/participant.service'; + +@Injectable() +export class FormSectionService { + constructor( + @InjectModel(FormSection.name) + private readonly formSectionModel: PaginateModel, + ) {} + + async create(data: CreateFormSectionDto): Promise { + const formSection = new this.formSectionModel({ + ...data, + id: data.id || undefined, // Let BaseEntity generate UUID if not provided + metadata: data.metadata || { entries: [] }, + }); + return formSection.save(); + } + + async findAll( + page = 1, + limit = 10, + ): Promise<{ + docs: FormSection[]; + totalDocs: number; + limit: number; + page: number; + totalPages: number; + hasNextPage: boolean; + hasPrevPage: boolean; + nextPage: number | null; + prevPage: number | null; + }> { + // Selects all fields from the FormSection entity for the response + return this.formSectionModel.paginate( + {}, + { page, limit, lean: true }, + ); + } + + async findById(id: string): Promise { + const formSection = await this.formSectionModel + .findOne({ id }) + .lean() + .exec(); + if (!formSection) { + throw new NotFoundException(`FormSection with ID "${id}" not found`); + } + return formSection; + } + + async update( + id: string, + data: UpdateFormSectionDto, + ): Promise { + const updatedFormSection = await this.formSectionModel + .findOneAndUpdate( + { id }, + { $set: { ...data, updatedAt: new Date() } }, + { new: true }, + ) + .lean() + .exec(); + + if (!updatedFormSection) { + throw new NotFoundException(`FormSection with ID "${id}" not found`); + } + return updatedFormSection; + } + + async remove(id: string): Promise { + const result = await this.formSectionModel.deleteOne({ id }).exec(); + if (result.deletedCount === 0) { + throw new NotFoundException(`FormSection with ID "${id}" not found`); + } + } +}