diff --git a/infra/database/migrations/1765133594521-Update.ts b/infra/database/migrations/1765133594521-Update.ts new file mode 100644 index 0000000..d7ccf38 --- /dev/null +++ b/infra/database/migrations/1765133594521-Update.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class Update1765133594521 implements MigrationInterface { + name = 'Update1765133594521' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`referrals\` DROP COLUMN \`date\``); + await queryRunner.query(`ALTER TABLE \`referrals\` ADD \`date\` timestamp NOT NULL`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`referrals\` DROP COLUMN \`date\``); + await queryRunner.query(`ALTER TABLE \`referrals\` ADD \`date\` date NOT NULL`); + } + +} diff --git a/infra/database/seed-dev.ts b/infra/database/seed-dev.ts index 16461e3..6035b53 100644 --- a/infra/database/seed-dev.ts +++ b/infra/database/seed-dev.ts @@ -3,6 +3,10 @@ import { hash } from 'bcryptjs'; import * as fs from 'fs'; import * as path from 'path'; +import { + REFERRAL_CATEGORIES, + REFERRAL_STATUSES, +} from '@/domain/enums/referrals'; import { APPOINTMENT_CONDITION, APPOINTMENT_STATUS, @@ -16,10 +20,6 @@ import { PATIENT_REQUIREMENT_STATUS, PATIENT_REQUIREMENT_TYPE, } from '@/domain/schemas/patient-requirement'; -import { - REFERRAL_CATEGORIES, - REFERRAL_STATUSES, -} from '@/domain/schemas/referral'; import { SPECIALIST_STATUS } from '@/domain/schemas/specialist'; import { USER_ROLES } from '@/domain/schemas/user'; @@ -258,6 +258,10 @@ async function main() { annotation: faker.datatype.boolean() ? faker.lorem.sentence() : null, referred_to: faker.person.fullName(), referred_by: faker.string.uuid(), + created_at: faker.date.between({ + from: fourMonthsAgo, + to: new Date(), + }), }); await referralRepository.save(referral); } diff --git a/src/app/http/patients/patients.module.ts b/src/app/http/patients/patients.module.ts index 9355686..929d363 100644 --- a/src/app/http/patients/patients.module.ts +++ b/src/app/http/patients/patients.module.ts @@ -19,6 +19,6 @@ import { PatientsService } from './patients.service'; ], controllers: [PatientsController], providers: [PatientsService, PatientsRepository], - exports: [PatientsRepository], + exports: [PatientsRepository, TypeOrmModule.forFeature([Patient])], }) export class PatientsModule {} diff --git a/src/app/http/patients/patients.repository.ts b/src/app/http/patients/patients.repository.ts index fffcd60..7d1bf0b 100644 --- a/src/app/http/patients/patients.repository.ts +++ b/src/app/http/patients/patients.repository.ts @@ -1,28 +1,10 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { - Between, - type FindOptionsWhere, - IsNull, - LessThanOrEqual, - MoreThanOrEqual, - Not, - Repository, - type SelectQueryBuilder, -} from 'typeorm'; +import { Repository } from 'typeorm'; import { Patient } from '@/domain/entities/patient'; -import type { - PatientOrderBy, - PatientStatus, - PatientType, -} from '@/domain/schemas/patient'; -import type { - PatientsStatisticField, - StateReferredPatients, -} from '@/domain/schemas/statistics'; +import type { PatientOrderBy, PatientType } from '@/domain/schemas/patient'; -import type { GetPatientsByPeriodQuery } from '../statistics/statistics.dtos'; import { CreatePatientDto, FindAllPatientQueryDto } from './patients.dtos'; @Injectable() @@ -177,169 +159,4 @@ export class PatientsRepository { public async deactivate(id: string): Promise { return this.patientsRepository.save({ id, status: 'inactive' }); } - - public async getTotalPatientsByStatus(): Promise<{ - total: number; - active: number; - inactive: number; - }> { - const queryBuilder = await this.patientsRepository - .createQueryBuilder('patient') - .select('COUNT(patient.id)', 'total') - .where('patient.status != :status', { status: 'pending' }) - .addSelect( - `SUM(CASE WHEN patient.status = 'active' THEN 1 ELSE 0 END)`, - 'active', - ) - .addSelect( - `SUM(CASE WHEN patient.status = 'inactive' THEN 1 ELSE 0 END)`, - 'inactive', - ) - .getRawOne<{ total: string; active: string; inactive: string }>(); - - return { - total: Number(queryBuilder?.total ?? 0), - active: Number(queryBuilder?.active ?? 0), - inactive: Number(queryBuilder?.inactive ?? 0), - }; - } - - public async getPatientsStatisticsByPeriod( - field: PatientsStatisticField, - startDate: Date, - endDate: Date, - query: GetPatientsByPeriodQuery, - ): Promise<{ items: T[]; total: number }> { - const totalQuery = this.patientsRepository - .createQueryBuilder('patient') - .select(`COUNT(DISTINCT patient.${field})`, 'total') - .where('patient.created_at BETWEEN :start AND :end', { - start: startDate, - end: endDate, - }); - - const totalResult = await totalQuery.getRawOne<{ total: string }>(); - const total = Number(totalResult?.total ?? 0); - - const queryBuilder = this.patientsRepository - .createQueryBuilder('patient') - .select(`patient.${field}`, field) - .addSelect('COUNT(patient.id)', 'total') - .where('patient.created_at BETWEEN :start AND :end', { - start: startDate, - end: endDate, - }) - .groupBy(`patient.${field}`) - .orderBy('total', query.order) - .limit(query.limit); - - if (query.withPercentage) { - queryBuilder.addSelect( - 'ROUND((COUNT(*) * 100.0 / SUM(COUNT(*)) OVER()), 1)', - 'percentage', - ); - } - - const items = await queryBuilder.getRawMany(); - - return { items, total }; - } - - public async getTotalPatients( - input: { status?: PatientStatus; startDate?: Date; endDate?: Date } = {}, - ): Promise { - const { status, startDate, endDate } = input; - - const where: FindOptionsWhere = { - status: status ?? Not('pending'), - }; - - if (startDate && !endDate) { - where.created_at = MoreThanOrEqual(startDate); - } - - if (endDate && !startDate) { - where.created_at = LessThanOrEqual(endDate); - } - - if (startDate && endDate) { - where.created_at = Between(startDate, endDate); - } - - return await this.patientsRepository.count({ where }); - } - - public async getTotalReferredPatients( - input: { startDate?: Date; endDate?: Date } = {}, - ): Promise { - const { startDate, endDate } = input; - - const where: FindOptionsWhere = { - referrals: { id: Not(IsNull()) }, - }; - - if (startDate && !endDate) { - where.created_at = MoreThanOrEqual(startDate); - } - - if (endDate && !startDate) { - where.created_at = LessThanOrEqual(endDate); - } - - if (startDate && endDate) { - where.created_at = Between(startDate, endDate); - } - - return await this.patientsRepository.count({ where }); - } - - public async getReferredPatientsByState( - input: { startDate?: Date; endDate?: Date; limit?: number } = {}, - ): Promise<{ states: StateReferredPatients[]; total: number }> { - const { startDate, endDate, limit = 10 } = input; - - const createQueryBuilder = (): SelectQueryBuilder => { - return this.patientsRepository - .createQueryBuilder('patient') - .innerJoin('patient.referrals', 'referral') - .where('referral.referred_to IS NOT NULL') - .andWhere('referral.referred_to != :empty', { empty: '' }); - }; - - function getQueryBuilderWithFilters( - queryBuilder: SelectQueryBuilder, - ) { - if (startDate && endDate) { - queryBuilder.andWhere('referral.date BETWEEN :start AND :end', { - start: startDate, - end: endDate, - }); - } - - return queryBuilder; - } - - const stateListQuery = getQueryBuilderWithFilters( - createQueryBuilder() - .select('patient.state', 'state') - .addSelect('COUNT(DISTINCT patient.id)', 'total') - .groupBy('patient.state') - .orderBy('COUNT(DISTINCT patient.id)', 'DESC') - .limit(limit), - ); - - const totalStatesQuery = getQueryBuilderWithFilters( - createQueryBuilder().select('COUNT(DISTINCT patient.state)', 'total'), - ); - - const [states, totalResult] = await Promise.all([ - stateListQuery.getRawMany(), - totalStatesQuery.getRawOne<{ total: string }>(), - ]); - - return { - states, - total: Number(totalResult?.total || 0), - }; - } } diff --git a/src/app/http/referrals/referrals.controller.ts b/src/app/http/referrals/referrals.controller.ts index 87db5aa..4a0dc8a 100644 --- a/src/app/http/referrals/referrals.controller.ts +++ b/src/app/http/referrals/referrals.controller.ts @@ -1,39 +1,72 @@ -import { Body, Controller, Param, Patch, Post } from '@nestjs/common'; +import { + Body, + Controller, + Get, + Param, + Patch, + Post, + Query, +} from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { CurrentUser } from '@/common/decorators/current-user.decorator'; import { Roles } from '@/common/decorators/roles.decorator'; import { BaseResponseSchema } from '@/domain/schemas/base'; +import type { GetReferralsResponseSchema } from '@/domain/schemas/referral'; import { UserSchema } from '@/domain/schemas/user'; -import { CreateReferralDto } from './referrals.dtos'; -import { ReferralsService } from './referrals.service'; +import { CreateReferralDto, GetReferralsQuery } from './referrals.dtos'; +import { CancelReferralUseCase } from './use-cases/cancel-referral.use-case'; +import { CreateReferralUseCase } from './use-cases/create-referrals.use-case'; +import { GetReferralsUseCase } from './use-cases/get-referrals.use-case'; @ApiTags('Encaminhamentos') @Controller('referrals') export class ReferralsController { - constructor(private readonly referralsService: ReferralsService) {} + constructor( + private readonly getReferralsUseCase: GetReferralsUseCase, + private readonly createReferralUseCase: CreateReferralUseCase, + private readonly cancelReferralUseCase: CancelReferralUseCase, + ) {} + + @Get() + @Roles(['manager', 'nurse']) + @ApiOperation({ summary: 'Lista encaminhamentos cadastrados no sistema' }) + async getReferrals( + @Query() query: GetReferralsQuery, + ): Promise { + const data = await this.getReferralsUseCase.execute({ query }); + + return { + success: true, + message: 'Lista de encaminhamentos retornada com sucesso.', + data, + }; + } @Post() @Roles(['manager', 'nurse']) - @ApiOperation({ summary: 'Cadastra novo encaminhamento.' }) + @ApiOperation({ summary: 'Cadastra um novo encaminhamento' }) async create( @Body() createReferralDto: CreateReferralDto, @CurrentUser() currentUser: UserSchema, ): Promise { - await this.referralsService.create(createReferralDto, currentUser.id); + await this.createReferralUseCase.execute({ + createReferralDto, + userId: currentUser.id, + }); return { success: true, message: 'Encaminhamento cadastrado com sucesso.' }; } @Patch(':id/cancel') @Roles(['nurse', 'manager', 'specialist']) - @ApiOperation({ summary: 'Cancela um encaminhamento.' }) + @ApiOperation({ summary: 'Cancela um encaminhamento' }) async cancel( @Param('id') id: string, @CurrentUser() user: UserSchema, ): Promise { - await this.referralsService.cancel(id, user); + await this.cancelReferralUseCase.execute({ id, userId: user.id }); return { success: true, diff --git a/src/app/http/referrals/referrals.dtos.ts b/src/app/http/referrals/referrals.dtos.ts index 4440849..e68bf20 100644 --- a/src/app/http/referrals/referrals.dtos.ts +++ b/src/app/http/referrals/referrals.dtos.ts @@ -1,5 +1,10 @@ import { createZodDto } from 'nestjs-zod'; -import { createReferralSchema } from '@/domain/schemas/referral'; +import { + createReferralSchema, + getReferralsQuerySchema, +} from '@/domain/schemas/referral'; export class CreateReferralDto extends createZodDto(createReferralSchema) {} + +export class GetReferralsQuery extends createZodDto(getReferralsQuerySchema) {} diff --git a/src/app/http/referrals/referrals.module.ts b/src/app/http/referrals/referrals.module.ts index 77c9b8d..c3f96c9 100644 --- a/src/app/http/referrals/referrals.module.ts +++ b/src/app/http/referrals/referrals.module.ts @@ -1,17 +1,24 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { Patient } from '@/domain/entities/patient'; import { Referral } from '@/domain/entities/referral'; -import { PatientsModule } from '../patients/patients.module'; import { ReferralsController } from './referrals.controller'; -import { ReferralsRepository } from './referrals.repository'; -import { ReferralsService } from './referrals.service'; +import { CancelReferralUseCase } from './use-cases/cancel-referral.use-case'; +import { CreateReferralUseCase } from './use-cases/create-referrals.use-case'; +import { GetReferralsUseCase } from './use-cases/get-referrals.use-case'; @Module({ - imports: [PatientsModule, TypeOrmModule.forFeature([Referral])], + imports: [ + TypeOrmModule.forFeature([Referral]), + TypeOrmModule.forFeature([Patient]), + ], controllers: [ReferralsController], - providers: [ReferralsService, ReferralsRepository], - exports: [ReferralsService, ReferralsRepository], + providers: [ + GetReferralsUseCase, + CreateReferralUseCase, + CancelReferralUseCase, + ], }) export class ReferralsModule {} diff --git a/src/app/http/referrals/referrals.repository.ts b/src/app/http/referrals/referrals.repository.ts deleted file mode 100644 index 61ee69d..0000000 --- a/src/app/http/referrals/referrals.repository.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { - Between, - type FindOptionsWhere, - LessThanOrEqual, - MoreThanOrEqual, - Repository, - type SelectQueryBuilder, -} from 'typeorm'; - -import { Referral } from '@/domain/entities/referral'; -import { ReferralStatus } from '@/domain/schemas/referral'; -import type { CategoryTotalReferrals } from '@/domain/schemas/statistics'; - -import { CreateReferralDto } from './referrals.dtos'; - -@Injectable() -export class ReferralsRepository { - constructor( - @InjectRepository(Referral) - private readonly referralsRepository: Repository, - ) {} - - public async findById(id: string): Promise { - return await this.referralsRepository.findOne({ where: { id } }); - } - - public async create( - createReferralDto: CreateReferralDto & { - status: ReferralStatus; - referred_by: string; - }, - ): Promise { - const referrals = this.referralsRepository.create(createReferralDto); - return await this.referralsRepository.save(referrals); - } - - public async cancel(id: string): Promise { - return await this.referralsRepository.save({ id, status: 'canceled' }); - } - - public async getTotalReferrals( - input: { - status?: ReferralStatus; - startDate?: Date; - endDate?: Date; - } = {}, - ): Promise { - const { status, startDate, endDate } = input; - - const where: FindOptionsWhere = {}; - - if (status) { - where.status = status; - } - - if (startDate && !endDate) { - where.date = MoreThanOrEqual(startDate); - } - - if (endDate && !startDate) { - where.date = LessThanOrEqual(endDate); - } - - if (startDate && endDate) { - where.date = Between(startDate, endDate); - } - - return await this.referralsRepository.count({ where }); - } - - public async getTotalReferralsByCategory( - input: { startDate?: Date; endDate?: Date; limit?: number } = {}, - ): Promise<{ categories: CategoryTotalReferrals[]; total: number }> { - const { startDate, endDate, limit = 10 } = input; - - function getQueryBuilderWithFilters( - queryBuilder: SelectQueryBuilder, - ) { - if (startDate && endDate) { - queryBuilder.andWhere('referral.date BETWEEN :start AND :end', { - start: startDate, - end: endDate, - }); - } - - return queryBuilder; - } - - const createQueryBuilder = (): SelectQueryBuilder => { - return this.referralsRepository.createQueryBuilder('referral'); - }; - - const categoryListQuery = getQueryBuilderWithFilters( - createQueryBuilder() - .select('referral.category', 'category') - .addSelect('COUNT(referral.id)', 'total') - .groupBy('referral.category') - .orderBy('COUNT(referral.id)', 'DESC') - .limit(limit), - ); - - const totalCategoriesQuery = getQueryBuilderWithFilters( - createQueryBuilder().select('COUNT(DISTINCT referral.category)', 'total'), - ); - - const [categories, totalResult] = await Promise.all([ - categoryListQuery.getRawMany(), - totalCategoriesQuery.getRawOne<{ total: string }>(), - ]); - - return { - categories, - total: Number(totalResult?.total || 0), - }; - } -} diff --git a/src/app/http/referrals/referrals.service.ts b/src/app/http/referrals/referrals.service.ts deleted file mode 100644 index 65f97d7..0000000 --- a/src/app/http/referrals/referrals.service.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { - BadRequestException, - Injectable, - Logger, - NotFoundException, -} from '@nestjs/common'; - -import { UserSchema } from '@/domain/schemas/user'; - -import { PatientsRepository } from '../patients/patients.repository'; -import { CreateReferralDto } from './referrals.dtos'; -import { ReferralsRepository } from './referrals.repository'; - -@Injectable() -export class ReferralsService { - private readonly logger = new Logger(ReferralsService.name); - - constructor( - private readonly referralsRepository: ReferralsRepository, - private readonly patientsRepository: PatientsRepository, - ) {} - - public async create( - createReferralDto: CreateReferralDto, - userId: string, - ): Promise { - const { patient_id } = createReferralDto; - - const patient = await this.patientsRepository.findById(patient_id); - - if (!patient) { - throw new NotFoundException('Paciente não encontrado.'); - } - - await this.referralsRepository.create({ - ...createReferralDto, - status: 'scheduled', - referred_by: userId, - }); - - this.logger.log( - { patientId: patient_id, referredBy: userId }, - 'Referral created successfully', - ); - } - - async cancel(id: string, user: UserSchema): Promise { - const referral = await this.referralsRepository.findById(id); - - if (!referral) { - throw new NotFoundException('Encaminhamento não encontrado.'); - } - - if (referral.status === 'canceled') { - throw new BadRequestException('Este encaminhamento já está cancelado.'); - } - - await this.referralsRepository.cancel(referral.id); - - this.logger.log( - { id: referral.id, userId: user.id }, - 'Referral canceled successfully.', - ); - } -} diff --git a/src/app/http/referrals/use-cases/cancel-referral.use-case.ts b/src/app/http/referrals/use-cases/cancel-referral.use-case.ts new file mode 100644 index 0000000..ee3c29e --- /dev/null +++ b/src/app/http/referrals/use-cases/cancel-referral.use-case.ts @@ -0,0 +1,49 @@ +import { + BadRequestException, + Injectable, + Logger, + NotFoundException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import type { Repository } from 'typeorm'; + +import { Referral } from '@/domain/entities/referral'; + +interface CancelReferralUseCaseRequest { + id: string; + userId: string; +} + +type CancelReferralUseCaseResponse = Promise; + +@Injectable() +export class CancelReferralUseCase { + private readonly logger = new Logger(CancelReferralUseCase.name); + + constructor( + @InjectRepository(Referral) + private readonly referralsRepository: Repository, + ) {} + + async execute({ + id, + userId, + }: CancelReferralUseCaseRequest): CancelReferralUseCaseResponse { + const referral = await this.referralsRepository.findOne({ + select: { id: true, status: true }, + where: { id }, + }); + + if (!referral) { + throw new NotFoundException('Encaminhamento não encontrado.'); + } + + if (referral.status === 'canceled') { + throw new BadRequestException('Este encaminhamento já está cancelado.'); + } + + await this.referralsRepository.save({ id, status: 'canceled' }); + + this.logger.log({ id, userId }, 'Referral canceled successfully.'); + } +} diff --git a/src/app/http/referrals/use-cases/create-referrals.use-case.ts b/src/app/http/referrals/use-cases/create-referrals.use-case.ts new file mode 100644 index 0000000..c60a49c --- /dev/null +++ b/src/app/http/referrals/use-cases/create-referrals.use-case.ts @@ -0,0 +1,54 @@ +import { Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { type Repository } from 'typeorm'; + +import { Patient } from '@/domain/entities/patient'; +import { Referral } from '@/domain/entities/referral'; + +import { CreateReferralDto } from '../referrals.dtos'; + +interface CreateReferralUseCaseRequest { + createReferralDto: CreateReferralDto; + userId: string; +} + +type CreateReferralUseCaseResponse = Promise; + +@Injectable() +export class CreateReferralUseCase { + private readonly logger = new Logger(CreateReferralUseCase.name); + + constructor( + @InjectRepository(Patient) + private readonly patientsRepository: Repository, + + @InjectRepository(Referral) + private readonly referralsRepository: Repository, + ) {} + async execute({ + createReferralDto, + userId, + }: CreateReferralUseCaseRequest): CreateReferralUseCaseResponse { + const { patient_id } = createReferralDto; + + const patient = await this.patientsRepository.findOne({ + where: { id: patient_id }, + select: { id: true }, + }); + + if (!patient) { + throw new NotFoundException('Paciente não encontrado.'); + } + + await this.referralsRepository.save({ + ...createReferralDto, + status: 'scheduled', + referred_by: userId, + }); + + this.logger.log( + { patientId: patient_id, referredBy: userId }, + 'Referral created successfully', + ); + } +} diff --git a/src/app/http/referrals/use-cases/get-referrals.use-case.ts b/src/app/http/referrals/use-cases/get-referrals.use-case.ts new file mode 100644 index 0000000..459bcf1 --- /dev/null +++ b/src/app/http/referrals/use-cases/get-referrals.use-case.ts @@ -0,0 +1,98 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { + Between, + type FindOptionsWhere, + ILike, + LessThanOrEqual, + MoreThanOrEqual, + type Repository, +} from 'typeorm'; + +import { Referral } from '@/domain/entities/referral'; +import type { GetReferralsResponseSchema } from '@/domain/schemas/referral'; + +import { GetReferralsQuery } from '../referrals.dtos'; + +interface GetReferralsUseCaseRequest { + query: GetReferralsQuery; +} + +type GetReferralsUseCaseResponse = Promise; + +@Injectable() +export class GetReferralsUseCase { + constructor( + @InjectRepository(Referral) + private readonly referralsRepository: Repository, + ) {} + + async execute({ + query, + }: GetReferralsUseCaseRequest): GetReferralsUseCaseResponse { + const { orderBy, page, perPage, category, condition, order, search } = + query; + + const where: FindOptionsWhere = {}; + const startDate = query.startDate ? new Date(query.startDate) : null; + const endDate = query.endDate ? new Date(query.endDate) : null; + + if (condition) { + where.condition = condition; + } + + if (category) { + where.category = category; + } + + if (startDate && !endDate) { + where.date = MoreThanOrEqual(startDate); + } + + if (endDate && !startDate) { + where.date = LessThanOrEqual(endDate); + } + + if (startDate && endDate) { + where.date = Between(startDate, endDate); + } + + if (search) { + where.patient = { user: { name: ILike(`%${search}%`) } }; + } + + const totalQuery = await this.referralsRepository.count({ where }); + + const referralsQuery = await this.referralsRepository.find({ + relations: { patient: { user: true } }, + select: { + patient: { + id: true, + user: { name: true, avatar_url: true }, + }, + }, + order: { [orderBy]: order }, + take: perPage, + skip: (page - 1) * perPage, + where, + }); + + const referrals = referralsQuery.map((referral) => ({ + id: referral.id, + date: referral.date, + category: referral.category, + condition: referral.condition, + annotation: referral.annotation, + status: referral.status, + referred_to: referral.referred_to, + created_at: referral.created_at, + patient: { + id: referral.patient.id, + name: referral.patient.user.name, + avatar_url: referral.patient.user.avatar_url, + }, + })); + + return { referrals, total: totalQuery }; + } +} diff --git a/src/app/http/statistics/statistics.controller.ts b/src/app/http/statistics/statistics.controller.ts index 271f910..fbdcced 100644 --- a/src/app/http/statistics/statistics.controller.ts +++ b/src/app/http/statistics/statistics.controller.ts @@ -8,28 +8,38 @@ import type { GetReferredPatientsByStateResponse, GetTotalReferralsAndReferredPatientsPercentageResponse, GetTotalReferralsByCategoryResponse, - PatientsByCity, - PatientsByGender, -} from '@/domain/schemas/statistics'; + TotalPatientsByCity, + TotalPatientsByGender, +} from '@/domain/schemas/statistics/responses'; import { - GetPatientsByPeriodQuery, GetReferredPatientsByStateQuery, + GetTotalPatientsByFieldQuery, GetTotalReferralsAndReferredPatientsPercentageQuery, GetTotalReferralsByCategoryQuery, } from './statistics.dtos'; -import { StatisticsService } from './statistics.service'; +import { GetTotalPatientsByFieldUseCase } from './use-cases/get-total-patients-by-field.use-case'; +import { GetTotalPatientsByStatusUseCase } from './use-cases/get-total-patients-by-status.use-case'; +import { GetTotalReferralsAndReferredPatientsPercentageUseCase } from './use-cases/get-total-referrals-and-referred-patients-percentage.use-case'; +import { GetTotalReferralsByCategoryUseCase } from './use-cases/get-total-referrals-by-category.use-case'; +import { GetTotalReferredPatientsByStateUseCase } from './use-cases/get-total-referred-patients-by-state.use-case'; @ApiTags('Estatísticas') +@Roles(['manager', 'nurse']) @Controller('statistics') export class StatisticsController { - constructor(private readonly statisticsService: StatisticsService) {} + constructor( + private readonly getTotalPatientsByStatusUseCase: GetTotalPatientsByStatusUseCase, + private readonly getTotalPatientsByPeriodUseCase: GetTotalPatientsByFieldUseCase, + private readonly getTotalReferredPatientsByStateUseCase: GetTotalReferredPatientsByStateUseCase, + private readonly getTotalReferralsByCategoryUseCase: GetTotalReferralsByCategoryUseCase, + private readonly getTotalReferralsAndReferredPatientsPercentageUseCase: GetTotalReferralsAndReferredPatientsPercentageUseCase, + ) {} - @Get('patients/total') - @Roles(['manager', 'nurse']) + @Get('patients-total') @ApiOperation({ summary: 'Estatísticas totais de pacientes' }) async getPatientsTotal() { - const data = await this.statisticsService.getPatientsTotal(); + const data = await this.getTotalPatientsByStatusUseCase.execute(); return { success: true, @@ -39,15 +49,16 @@ export class StatisticsController { } @Get('patients-by-gender') - @Roles(['manager', 'nurse']) @ApiOperation({ summary: 'Estatísticas de pacientes por gênero' }) async getPatientsByGender( - @Query() query: GetPatientsByPeriodQuery, + @Query() query: GetTotalPatientsByFieldQuery, ): Promise { const { items: genders, total } = - await this.statisticsService.getPatientsByPeriod( - 'gender', - query, + await this.getTotalPatientsByPeriodUseCase.execute( + { + field: 'gender', + query, + }, ); return { @@ -58,16 +69,15 @@ export class StatisticsController { } @Get('patients-by-city') - @Roles(['manager', 'nurse']) @ApiOperation({ summary: 'Estatísticas de pacientes por cidade' }) async getPatientsByCity( - @Query() query: GetPatientsByPeriodQuery, + @Query() query: GetTotalPatientsByFieldQuery, ): Promise { const { items: cities, total } = - await this.statisticsService.getPatientsByPeriod( - 'city', + await this.getTotalPatientsByPeriodUseCase.execute({ + field: 'city', query, - ); + }); return { success: true, @@ -77,15 +87,14 @@ export class StatisticsController { } @Get('referrals-total') - @Roles(['manager', 'nurse']) @ApiOperation({ summary: 'Estatísticas do total de encaminhamentos' }) async getTotalReferralsAndReferredPatientsPercentage( @Query() query: GetTotalReferralsAndReferredPatientsPercentageQuery, ): Promise { const data = - await this.statisticsService.getTotalReferralsAndReferredPatientsPercentage( + await this.getTotalReferralsAndReferredPatientsPercentageUseCase.execute({ query, - ); + }); return { success: true, @@ -96,7 +105,6 @@ export class StatisticsController { } @Get('referrals-by-category') - @Roles(['manager', 'nurse']) @ApiOperation({ summary: 'Lista com o total de encaminhamentos por categoria', }) @@ -104,7 +112,7 @@ export class StatisticsController { @Query() query: GetTotalReferralsByCategoryQuery, ): Promise { const { categories, total } = - await this.statisticsService.getTotalReferralsByCategory(query); + await this.getTotalReferralsByCategoryUseCase.execute({ query }); return { success: true, @@ -115,7 +123,6 @@ export class StatisticsController { } @Get('referrals-by-state') - @Roles(['manager', 'nurse']) @ApiOperation({ summary: 'Lista com o total de pacientes encaminhados por estado', }) @@ -123,7 +130,7 @@ export class StatisticsController { @Query() query: GetReferredPatientsByStateQuery, ): Promise { const { states, total } = - await this.statisticsService.getReferredPatientsByState(query); + await this.getTotalReferredPatientsByStateUseCase.execute({ query }); return { success: true, diff --git a/src/app/http/statistics/statistics.dtos.ts b/src/app/http/statistics/statistics.dtos.ts index 9004d2d..312c8e5 100644 --- a/src/app/http/statistics/statistics.dtos.ts +++ b/src/app/http/statistics/statistics.dtos.ts @@ -1,14 +1,14 @@ import { createZodDto } from 'nestjs-zod'; import { - getPatientsByPeriodQuerySchema, getReferredPatientsByStateQuerySchema, + getTotalPatientsByFieldQuerySchema, getTotalReferralsAndReferredPatientsPercentageQuerySchema, getTotalReferralsByCategoryQuerySchema, -} from '@/domain/schemas/statistics'; +} from '@/domain/schemas/statistics/requests'; -export class GetPatientsByPeriodQuery extends createZodDto( - getPatientsByPeriodQuerySchema, +export class GetTotalPatientsByFieldQuery extends createZodDto( + getTotalPatientsByFieldQuerySchema, ) {} export class GetTotalReferralsAndReferredPatientsPercentageQuery extends createZodDto( diff --git a/src/app/http/statistics/statistics.module.ts b/src/app/http/statistics/statistics.module.ts index 2c9cd33..99f4338 100644 --- a/src/app/http/statistics/statistics.module.ts +++ b/src/app/http/statistics/statistics.module.ts @@ -1,15 +1,32 @@ import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Patient } from '@/domain/entities/patient'; +import { Referral } from '@/domain/entities/referral'; import { UtilsModule } from '@/utils/utils.module'; -import { PatientsModule } from '../patients/patients.module'; -import { ReferralsModule } from '../referrals/referrals.module'; import { StatisticsController } from './statistics.controller'; -import { StatisticsService } from './statistics.service'; +import { GetTotalPatientsUseCase } from './use-cases/get-total-patients.use-case'; +import { GetTotalPatientsByFieldUseCase } from './use-cases/get-total-patients-by-field.use-case'; +import { GetTotalPatientsByStatusUseCase } from './use-cases/get-total-patients-by-status.use-case'; +import { GetTotalReferralsUseCase } from './use-cases/get-total-referrals.use-case'; +import { GetTotalReferralsAndReferredPatientsPercentageUseCase } from './use-cases/get-total-referrals-and-referred-patients-percentage.use-case'; +import { GetTotalReferralsByCategoryUseCase } from './use-cases/get-total-referrals-by-category.use-case'; +import { GetTotalReferredPatientsUseCase } from './use-cases/get-total-referred-patients.use-case'; +import { GetTotalReferredPatientsByStateUseCase } from './use-cases/get-total-referred-patients-by-state.use-case'; @Module({ - imports: [PatientsModule, UtilsModule, ReferralsModule], + imports: [TypeOrmModule.forFeature([Patient, Referral]), UtilsModule], controllers: [StatisticsController], - providers: [StatisticsService], + providers: [ + GetTotalPatientsUseCase, + GetTotalPatientsByFieldUseCase, + GetTotalPatientsByStatusUseCase, + GetTotalReferralsUseCase, + GetTotalReferralsByCategoryUseCase, + GetTotalReferralsAndReferredPatientsPercentageUseCase, + GetTotalReferredPatientsUseCase, + GetTotalReferredPatientsByStateUseCase, + ], }) export class StatisticsModule {} diff --git a/src/app/http/statistics/statistics.service.ts b/src/app/http/statistics/statistics.service.ts deleted file mode 100644 index 7fa5ffd..0000000 --- a/src/app/http/statistics/statistics.service.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import type { - CategoryTotalReferrals, - GetTotalPatientsByStatusResponse, - PatientsStatisticField, - StateReferredPatients, -} from '@/domain/schemas/statistics'; -import { UtilsService } from '@/utils/utils.service'; - -import { PatientsRepository } from '../patients/patients.repository'; -import { ReferralsRepository } from '../referrals/referrals.repository'; -import type { - GetPatientsByPeriodQuery, - GetReferredPatientsByStateQuery, - GetTotalReferralsAndReferredPatientsPercentageQuery, - GetTotalReferralsByCategoryQuery, -} from './statistics.dtos'; - -@Injectable() -export class StatisticsService { - constructor( - private readonly patientsRepository: PatientsRepository, - private readonly utilsService: UtilsService, - private readonly referralsRepository: ReferralsRepository, - ) {} - - async getPatientsTotal(): Promise { - return await this.patientsRepository.getTotalPatientsByStatus(); - } - - async getPatientsByPeriod( - filter: PatientsStatisticField, - query: GetPatientsByPeriodQuery, - ): Promise<{ items: T[]; total: number }> { - const { startDate, endDate } = this.utilsService.getDateRangeForPeriod( - query.period, - ); - - return await this.patientsRepository.getPatientsStatisticsByPeriod( - filter, - startDate, - endDate, - query, - ); - } - - async getTotalReferralsAndReferredPatientsPercentage( - query: GetTotalReferralsAndReferredPatientsPercentageQuery, - ): Promise<{ totalReferrals: number; referredPatientsPercentage: number }> { - const { startDate, endDate } = this.utilsService.getDateRangeForPeriod( - query.period, - ); - - const [totalPatients, totalReferrals, totalReferredPatients] = - await Promise.all([ - this.patientsRepository.getTotalPatients({ startDate, endDate }), - this.referralsRepository.getTotalReferrals({ startDate, endDate }), - this.patientsRepository.getTotalReferredPatients({ - startDate, - endDate, - }), - ]); - - const percentage = - Number((totalReferredPatients / totalPatients) * 100) || 0; - - return { - totalReferrals, - referredPatientsPercentage: Number(percentage.toFixed(2)), - }; - } - - async getTotalReferralsByCategory( - query: GetTotalReferralsByCategoryQuery, - ): Promise<{ categories: CategoryTotalReferrals[]; total: number }> { - const { startDate, endDate } = this.utilsService.getDateRangeForPeriod( - query.period, - ); - - return await this.referralsRepository.getTotalReferralsByCategory({ - startDate, - endDate, - }); - } - - async getReferredPatientsByState( - query: GetReferredPatientsByStateQuery, - ): Promise<{ states: StateReferredPatients[]; total: number }> { - const { startDate, endDate } = this.utilsService.getDateRangeForPeriod( - query.period, - ); - - return await this.patientsRepository.getReferredPatientsByState({ - startDate, - endDate, - }); - } -} diff --git a/src/app/http/statistics/use-cases/get-total-patients-by-field.use-case.ts b/src/app/http/statistics/use-cases/get-total-patients-by-field.use-case.ts new file mode 100644 index 0000000..03c043a --- /dev/null +++ b/src/app/http/statistics/use-cases/get-total-patients-by-field.use-case.ts @@ -0,0 +1,79 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import type { Repository, SelectQueryBuilder } from 'typeorm'; + +import { Patient } from '@/domain/entities/patient'; +import type { PatientsStatisticField } from '@/domain/enums/statistics'; +import { UtilsService } from '@/utils/utils.service'; + +import type { GetTotalPatientsByFieldQuery } from '../statistics.dtos'; +import { GetTotalPatientsUseCase } from './get-total-patients.use-case'; + +interface GetTotalPatientsByFieldUseCaseRequest { + field: PatientsStatisticField; + query: GetTotalPatientsByFieldQuery; +} + +type GetTotalPatientsByFieldUseCaseResponse = Promise<{ + items: T[]; + total: number; +}>; + +@Injectable() +export class GetTotalPatientsByFieldUseCase { + constructor( + @InjectRepository(Patient) + private readonly patientsRepository: Repository, + private readonly getTotalPatientsUseCase: GetTotalPatientsUseCase, + private readonly utilsService: UtilsService, + ) {} + + async execute({ + field, + query, + }: GetTotalPatientsByFieldUseCaseRequest): GetTotalPatientsByFieldUseCaseResponse { + const { startDate, endDate } = this.utilsService.getDateRangeForPeriod( + query.period, + ); + + const totalPatients = await this.getTotalPatientsUseCase.execute({ + startDate, + endDate, + }); + + const createBaseQuery = (): SelectQueryBuilder => { + return this.patientsRepository + .createQueryBuilder('patient') + .where('patient.created_at BETWEEN :start AND :end', { + start: startDate, + end: endDate, + }); + }; + + const totalFieldQuery = createBaseQuery().select( + `COUNT(DISTINCT patient.${field})`, + 'total', + ); + + const fieldQuery = createBaseQuery() + .select(`patient.${field}`, field) + .addSelect('COUNT(patient.id)', 'total') + .groupBy(`patient.${field}`) + .orderBy('total', query.order) + .limit(query.limit); + + if (query.withPercentage) { + fieldQuery.addSelect( + `ROUND((COUNT(patient.id) * 100.0 / ${totalPatients}), 1)`, + 'percentage', + ); + } + + const [items, totalResult] = await Promise.all([ + fieldQuery.getRawMany(), + totalFieldQuery.getRawOne<{ total: string }>(), + ]); + + return { items, total: Number(totalResult?.total || 0) }; + } +} diff --git a/src/app/http/statistics/use-cases/get-total-patients-by-status.use-case.ts b/src/app/http/statistics/use-cases/get-total-patients-by-status.use-case.ts new file mode 100644 index 0000000..2048bf8 --- /dev/null +++ b/src/app/http/statistics/use-cases/get-total-patients-by-status.use-case.ts @@ -0,0 +1,40 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import type { Repository } from 'typeorm'; + +import { Patient } from '@/domain/entities/patient'; +import type { GetTotalPatientsByStatusResponse } from '@/domain/schemas/statistics/responses'; + +type GetTotalPatientsByStatusUseCaseResponse = Promise< + GetTotalPatientsByStatusResponse['data'] +>; + +@Injectable() +export class GetTotalPatientsByStatusUseCase { + constructor( + @InjectRepository(Patient) + private readonly patientsRepository: Repository, + ) {} + + async execute(): GetTotalPatientsByStatusUseCaseResponse { + const queryBuilder = await this.patientsRepository + .createQueryBuilder('patient') + .select('COUNT(patient.id)', 'total') + .where('patient.status != :status', { status: 'pending' }) + .addSelect( + `SUM(CASE WHEN patient.status = 'active' THEN 1 ELSE 0 END)`, + 'active', + ) + .addSelect( + `SUM(CASE WHEN patient.status = 'inactive' THEN 1 ELSE 0 END)`, + 'inactive', + ) + .getRawOne<{ total: string; active: string; inactive: string }>(); + + return { + total: Number(queryBuilder?.total ?? 0), + active: Number(queryBuilder?.active ?? 0), + inactive: Number(queryBuilder?.inactive ?? 0), + }; + } +} diff --git a/src/app/http/statistics/use-cases/get-total-patients.use-case.ts b/src/app/http/statistics/use-cases/get-total-patients.use-case.ts new file mode 100644 index 0000000..be41b60 --- /dev/null +++ b/src/app/http/statistics/use-cases/get-total-patients.use-case.ts @@ -0,0 +1,63 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { + Between, + type FindOptionsWhere, + LessThanOrEqual, + MoreThanOrEqual, + Not, + type Repository, +} from 'typeorm'; + +import { Patient } from '@/domain/entities/patient'; +import type { PatientStatus } from '@/domain/schemas/patient'; +import type { QueryPeriod } from '@/domain/schemas/query'; +import { UtilsService } from '@/utils/utils.service'; + +interface GetTotalPatientsUseCaseRequest { + status?: PatientStatus; + period?: QueryPeriod; + startDate?: Date; + endDate?: Date; +} + +type GetTotalPatientsUseCaseResponse = Promise; + +@Injectable() +export class GetTotalPatientsUseCase { + constructor( + @InjectRepository(Patient) + private readonly patientsRepository: Repository, + private readonly utilsService: UtilsService, + ) {} + + async execute({ + status, + period, + startDate, + endDate, + }: GetTotalPatientsUseCaseRequest = {}): GetTotalPatientsUseCaseResponse { + const where: FindOptionsWhere = { + status: status ?? Not('pending'), + }; + + if (period) { + const dateRange = this.utilsService.getDateRangeForPeriod(period); + where.created_at = Between(dateRange.startDate, dateRange.endDate); + } + + if (startDate && !endDate) { + where.created_at = MoreThanOrEqual(startDate); + } + + if (endDate && !startDate) { + where.created_at = LessThanOrEqual(endDate); + } + + if (startDate && endDate) { + where.created_at = Between(startDate, endDate); + } + + return await this.patientsRepository.count({ select: { id: true }, where }); + } +} diff --git a/src/app/http/statistics/use-cases/get-total-referrals-and-referred-patients-percentage.use-case.ts b/src/app/http/statistics/use-cases/get-total-referrals-and-referred-patients-percentage.use-case.ts new file mode 100644 index 0000000..7491320 --- /dev/null +++ b/src/app/http/statistics/use-cases/get-total-referrals-and-referred-patients-percentage.use-case.ts @@ -0,0 +1,44 @@ +import { Injectable } from '@nestjs/common'; + +import type { GetTotalReferralsAndReferredPatientsPercentageQuery } from '../statistics.dtos'; +import { GetTotalPatientsUseCase } from './get-total-patients.use-case'; +import { GetTotalReferralsUseCase } from './get-total-referrals.use-case'; +import { GetTotalReferredPatientsUseCase } from './get-total-referred-patients.use-case'; + +interface GetTotalReferralsAndReferredPatientsPercentageUseCaseRequest { + query: GetTotalReferralsAndReferredPatientsPercentageQuery; +} + +type GetTotalReferralsAndReferredPatientsPercentageUseCaseResponse = Promise<{ + totalReferrals: number; + referredPatientsPercentage: number; +}>; + +@Injectable() +export class GetTotalReferralsAndReferredPatientsPercentageUseCase { + constructor( + private readonly getTotalPatientsUseCase: GetTotalPatientsUseCase, + private readonly getTotalReferralsUseCase: GetTotalReferralsUseCase, + private readonly getTotalReferredPatientsUseCase: GetTotalReferredPatientsUseCase, + ) {} + + async execute({ + query, + }: GetTotalReferralsAndReferredPatientsPercentageUseCaseRequest): GetTotalReferralsAndReferredPatientsPercentageUseCaseResponse { + const { period } = query; + + const [totalPatients, totalReferrals, totalReferredPatients] = + await Promise.all([ + this.getTotalPatientsUseCase.execute({ period }), + this.getTotalReferralsUseCase.execute({ period }), + this.getTotalReferredPatientsUseCase.execute({ period }), + ]); + + const percentage = Number((totalReferredPatients / totalPatients) * 100); + + return { + totalReferrals, + referredPatientsPercentage: Number(percentage.toFixed(2)), + }; + } +} diff --git a/src/app/http/statistics/use-cases/get-total-referrals-by-category.use-case.ts b/src/app/http/statistics/use-cases/get-total-referrals-by-category.use-case.ts new file mode 100644 index 0000000..6db9d54 --- /dev/null +++ b/src/app/http/statistics/use-cases/get-total-referrals-by-category.use-case.ts @@ -0,0 +1,75 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import type { Repository, SelectQueryBuilder } from 'typeorm'; + +import { Referral } from '@/domain/entities/referral'; +import type { TotalReferralsByCategory } from '@/domain/schemas/statistics/responses'; +import { UtilsService } from '@/utils/utils.service'; + +import type { GetTotalReferralsByCategoryQuery } from '../statistics.dtos'; + +interface GetTotalReferralsByCategoryUseCaseRequest { + query: GetTotalReferralsByCategoryQuery; +} + +type GetTotalReferralsByCategoryUseCaseResponse = Promise<{ + categories: TotalReferralsByCategory[]; + total: number; +}>; + +@Injectable() +export class GetTotalReferralsByCategoryUseCase { + constructor( + @InjectRepository(Referral) + private readonly referralsRepository: Repository, + private readonly utilsService: UtilsService, + ) {} + + async execute({ + query, + }: GetTotalReferralsByCategoryUseCaseRequest): GetTotalReferralsByCategoryUseCaseResponse { + const { startDate, endDate } = this.utilsService.getDateRangeForPeriod( + query.period, + ); + + const createQueryBuilder = (): SelectQueryBuilder => { + return this.referralsRepository.createQueryBuilder('referral'); + }; + + function getQueryBuilderWithFilters( + queryBuilder: SelectQueryBuilder, + ) { + if (startDate && endDate) { + queryBuilder.andWhere('referral.date BETWEEN :start AND :end', { + start: startDate, + end: endDate, + }); + } + + return queryBuilder; + } + + const categoryListQuery = getQueryBuilderWithFilters( + createQueryBuilder() + .select('referral.category', 'category') + .addSelect('COUNT(referral.id)', 'total') + .groupBy('referral.category') + .orderBy('COUNT(referral.id)', 'DESC') + .limit(query.limit), + ); + + const totalCategoriesQuery = getQueryBuilderWithFilters( + createQueryBuilder().select('COUNT(DISTINCT referral.category)', 'total'), + ); + + const [categories, totalResult] = await Promise.all([ + categoryListQuery.getRawMany(), + totalCategoriesQuery.getRawOne<{ total: string }>(), + ]); + + return { + categories, + total: Number(totalResult?.total || 0), + }; + } +} diff --git a/src/app/http/statistics/use-cases/get-total-referrals.use-case.ts b/src/app/http/statistics/use-cases/get-total-referrals.use-case.ts new file mode 100644 index 0000000..83fea04 --- /dev/null +++ b/src/app/http/statistics/use-cases/get-total-referrals.use-case.ts @@ -0,0 +1,83 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { + Between, + type FindOptionsWhere, + LessThanOrEqual, + MoreThanOrEqual, + type Repository, +} from 'typeorm'; + +import { Referral } from '@/domain/entities/referral'; +import type { + ReferralCategory, + ReferralStatus, +} from '@/domain/enums/referrals'; +import type { PatientCondition } from '@/domain/schemas/patient'; +import type { QueryPeriod } from '@/domain/schemas/query'; +import { UtilsService } from '@/utils/utils.service'; + +interface GetTotalReferralsUseCaseRequest { + status?: ReferralStatus; + category?: ReferralCategory; + condition?: PatientCondition; + period?: QueryPeriod; + startDate?: Date; + endDate?: Date; +} + +type GetTotalReferralsUseCaseResponse = Promise; + +@Injectable() +export class GetTotalReferralsUseCase { + constructor( + @InjectRepository(Referral) + private readonly referralsRepository: Repository, + private readonly utilsService: UtilsService, + ) {} + + async execute({ + status, + category, + condition, + period, + startDate, + endDate, + }: GetTotalReferralsUseCaseRequest = {}): GetTotalReferralsUseCaseResponse { + const where: FindOptionsWhere = {}; + + if (period) { + const dateRange = this.utilsService.getDateRangeForPeriod(period); + where.created_at = Between(dateRange.startDate, dateRange.endDate); + } + + if (startDate && !endDate) { + where.created_at = MoreThanOrEqual(startDate); + } + + if (endDate && !startDate) { + where.created_at = LessThanOrEqual(endDate); + } + + if (startDate && endDate) { + where.created_at = Between(startDate, endDate); + } + + if (status) { + where.status = status; + } + + if (category) { + where.category = category; + } + + if (condition) { + where.condition = condition; + } + + return await this.referralsRepository.count({ + select: { id: true }, + where, + }); + } +} diff --git a/src/app/http/statistics/use-cases/get-total-referred-patients-by-state.use-case.ts b/src/app/http/statistics/use-cases/get-total-referred-patients-by-state.use-case.ts new file mode 100644 index 0000000..f1cc30b --- /dev/null +++ b/src/app/http/statistics/use-cases/get-total-referred-patients-by-state.use-case.ts @@ -0,0 +1,79 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import type { Repository, SelectQueryBuilder } from 'typeorm'; + +import { Patient } from '@/domain/entities/patient'; +import type { TotalReferredPatientsByStateSchema } from '@/domain/schemas/statistics/responses'; +import { UtilsService } from '@/utils/utils.service'; + +import type { GetReferredPatientsByStateQuery } from '../statistics.dtos'; + +interface GetTotalReferredPatientsByStateUseCaseRequest { + query: GetReferredPatientsByStateQuery; +} + +type GetTotalReferredPatientsByStateUseCaseResponse = Promise<{ + states: TotalReferredPatientsByStateSchema[]; + total: number; +}>; + +@Injectable() +export class GetTotalReferredPatientsByStateUseCase { + constructor( + @InjectRepository(Patient) + private readonly patientsRepository: Repository, + private readonly utilsService: UtilsService, + ) {} + + async execute({ + query, + }: GetTotalReferredPatientsByStateUseCaseRequest): GetTotalReferredPatientsByStateUseCaseResponse { + const { startDate, endDate } = this.utilsService.getDateRangeForPeriod( + query.period, + ); + + const createQueryBuilder = (): SelectQueryBuilder => { + return this.patientsRepository + .createQueryBuilder('patient') + .innerJoin('patient.referrals', 'referral') + .where('referral.referred_to IS NOT NULL') + .andWhere('referral.referred_to != :empty', { empty: '' }); + }; + + function getQueryBuilderWithFilters( + queryBuilder: SelectQueryBuilder, + ) { + if (startDate && endDate) { + queryBuilder.andWhere('referral.date BETWEEN :start AND :end', { + start: startDate, + end: endDate, + }); + } + + return queryBuilder; + } + + const stateListQuery = getQueryBuilderWithFilters( + createQueryBuilder() + .select('patient.state', 'state') + .addSelect('COUNT(DISTINCT patient.id)', 'total') + .groupBy('patient.state') + .orderBy('COUNT(DISTINCT patient.id)', 'DESC') + .limit(query.limit), + ); + + const totalStatesQuery = getQueryBuilderWithFilters( + createQueryBuilder().select('COUNT(DISTINCT patient.state)', 'total'), + ); + + const [states, totalResult] = await Promise.all([ + stateListQuery.getRawMany(), + totalStatesQuery.getRawOne<{ total: string }>(), + ]); + + return { + states, + total: Number(totalResult?.total || 0), + }; + } +} diff --git a/src/app/http/statistics/use-cases/get-total-referred-patients.use-case.ts b/src/app/http/statistics/use-cases/get-total-referred-patients.use-case.ts new file mode 100644 index 0000000..d64888f --- /dev/null +++ b/src/app/http/statistics/use-cases/get-total-referred-patients.use-case.ts @@ -0,0 +1,61 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { + Between, + type FindOptionsWhere, + IsNull, + LessThanOrEqual, + MoreThanOrEqual, + Not, + type Repository, +} from 'typeorm'; + +import { Patient } from '@/domain/entities/patient'; +import type { QueryPeriod } from '@/domain/schemas/query'; +import { UtilsService } from '@/utils/utils.service'; + +interface GetTotalReferredPatientsUseCaseRequest { + period?: QueryPeriod; + startDate?: Date; + endDate?: Date; +} + +type GetTotalReferredPatientsUseCaseResponse = Promise; + +@Injectable() +export class GetTotalReferredPatientsUseCase { + constructor( + @InjectRepository(Patient) + private readonly patientsRepository: Repository, + private readonly utilsService: UtilsService, + ) {} + + async execute({ + period, + startDate, + endDate, + }: GetTotalReferredPatientsUseCaseRequest = {}): GetTotalReferredPatientsUseCaseResponse { + const where: FindOptionsWhere = { + referrals: { id: Not(IsNull()) }, + }; + + if (period) { + const dateRange = this.utilsService.getDateRangeForPeriod(period); + where.created_at = Between(dateRange.startDate, dateRange.endDate); + } + + if (startDate && !endDate) { + where.created_at = MoreThanOrEqual(startDate); + } + + if (endDate && !startDate) { + where.created_at = LessThanOrEqual(endDate); + } + + if (startDate && endDate) { + where.created_at = Between(startDate, endDate); + } + + return await this.patientsRepository.count({ where }); + } +} diff --git a/src/domain/entities/referral.ts b/src/domain/entities/referral.ts index d8ea942..6479321 100644 --- a/src/domain/entities/referral.ts +++ b/src/domain/entities/referral.ts @@ -8,14 +8,14 @@ import { UpdateDateColumn, } from 'typeorm'; -import { PATIENT_CONDITIONS, PatientCondition } from '../schemas/patient'; import { REFERRAL_CATEGORIES, REFERRAL_STATUSES, - ReferralCategory, - ReferralSchema, - ReferralStatus, -} from '../schemas/referral'; + type ReferralCategory, + type ReferralStatus, +} from '../enums/referrals'; +import { PATIENT_CONDITIONS, PatientCondition } from '../schemas/patient'; +import { ReferralSchema } from '../schemas/referral'; import { Patient } from './patient'; @Entity('referrals') @@ -26,7 +26,7 @@ export class Referral implements ReferralSchema { @Column('uuid') patient_id: string; - @Column({ type: 'date' }) + @Column({ type: 'timestamp' }) date: Date; @Column({ type: 'enum', enum: REFERRAL_CATEGORIES }) diff --git a/src/domain/enums/referrals.ts b/src/domain/enums/referrals.ts new file mode 100644 index 0000000..5bc8897 --- /dev/null +++ b/src/domain/enums/referrals.ts @@ -0,0 +1,29 @@ +export const REFERRAL_STATUSES = [ + 'scheduled', + 'canceled', + 'completed', + 'no_show', +] as const; +export type ReferralStatus = (typeof REFERRAL_STATUSES)[number]; + +export const REFERRAL_CATEGORIES = [ + 'medical_care', + 'legal', + 'nursing', + 'psychology', + 'nutrition', + 'physical_training', + 'social_work', + 'psychiatry', + 'neurology', + 'ophthalmology', +] as const; +export type ReferralCategory = (typeof REFERRAL_CATEGORIES)[number]; + +export const REFERRAL_ORDER_BY = [ + 'name', + 'condition', + 'category', + 'date', +] as const; +export type ReferralOrderBy = (typeof REFERRAL_ORDER_BY)[number]; diff --git a/src/domain/enums/statistics.ts b/src/domain/enums/statistics.ts new file mode 100644 index 0000000..d2705d9 --- /dev/null +++ b/src/domain/enums/statistics.ts @@ -0,0 +1,2 @@ +export const PATIENTS_STATISTIC_FIELDS = ['gender', 'city', 'state'] as const; +export type PatientsStatisticField = (typeof PATIENTS_STATISTIC_FIELDS)[number]; diff --git a/src/domain/schemas/referral.ts b/src/domain/schemas/referral.ts index ac9aeca..d8afe13 100644 --- a/src/domain/schemas/referral.ts +++ b/src/domain/schemas/referral.ts @@ -1,28 +1,13 @@ import { z } from 'zod'; +import { + REFERRAL_CATEGORIES, + REFERRAL_ORDER_BY, + REFERRAL_STATUSES, +} from '../enums/referrals'; +import { baseResponseSchema } from './base'; import { PATIENT_CONDITIONS } from './patient'; - -export const REFERRAL_STATUSES = [ - 'scheduled', - 'canceled', - 'completed', - 'no_show', -] as const; -export type ReferralStatus = (typeof REFERRAL_STATUSES)[number]; - -export const REFERRAL_CATEGORIES = [ - 'medical_care', - 'legal', - 'nursing', - 'psychology', - 'nutrition', - 'physical_training', - 'social_work', - 'psychiatry', - 'neurology', - 'ophthalmology', -] as const; -export type ReferralCategory = (typeof REFERRAL_CATEGORIES)[number]; +import { baseQuerySchema, QUERY_ORDER } from './query'; export const referralSchema = z .object({ @@ -31,7 +16,7 @@ export const referralSchema = z date: z.coerce.date(), category: z.enum(REFERRAL_CATEGORIES), condition: z.enum(PATIENT_CONDITIONS), - annotation: z.string().max(500).nullable(), + annotation: z.string().max(2000).nullable(), status: z.enum(REFERRAL_STATUSES).default('scheduled'), referred_to: z.string().nullable(), referred_by: z.string().uuid().nullable(), @@ -50,3 +35,51 @@ export const createReferralSchema = referralSchema.pick({ referred_to: true, }); export type CreateReferralSchema = z.infer; + +export const getReferralsQuerySchema = baseQuerySchema + .pick({ + search: true, + startDate: true, + endDate: true, + page: true, + perPage: true, + }) + .extend({ + category: z.enum(REFERRAL_CATEGORIES).optional(), + condition: z.enum(PATIENT_CONDITIONS).optional(), + order: z.enum(QUERY_ORDER).optional().default('DESC'), + orderBy: z.enum(REFERRAL_ORDER_BY).optional().default('date'), + }) + .refine( + (data) => { + if (data.startDate && data.endDate) { + return data.startDate < data.endDate; + } + return true; + }, + { + message: 'It should be greater than `startDate`', + path: ['endDate'], + }, + ); +export type GetReferralsQuerySchema = z.infer; + +export const getReferralsResponseSchema = baseResponseSchema.extend({ + data: z.object({ + referrals: z.array( + referralSchema + .omit({ patient_id: true, updated_at: true, referred_by: true }) + .extend({ + patient: z.object({ + id: z.string(), + name: z.string(), + avatar_url: z.string().nullable(), + }), + }), + ), + total: z.number(), + }), +}); +export type GetReferralsResponseSchema = z.infer< + typeof getReferralsResponseSchema +>; diff --git a/src/domain/schemas/statistics/requests.ts b/src/domain/schemas/statistics/requests.ts new file mode 100644 index 0000000..b197cdc --- /dev/null +++ b/src/domain/schemas/statistics/requests.ts @@ -0,0 +1,22 @@ +import { baseQuerySchema } from '../query'; + +// Patients + +export const getTotalPatientsByFieldQuerySchema = baseQuerySchema + .pick({ period: true, limit: true, order: true, withPercentage: true }) + .extend({ order: baseQuerySchema.shape.order.default('DESC') }); + +// Referrals + +export const getTotalReferralsAndReferredPatientsPercentageQuerySchema = + baseQuerySchema.pick({ period: true }); + +export const getReferredPatientsByStateQuerySchema = baseQuerySchema.pick({ + period: true, + limit: true, +}); + +export const getTotalReferralsByCategoryQuerySchema = baseQuerySchema.pick({ + period: true, + limit: true, +}); diff --git a/src/domain/schemas/statistics.ts b/src/domain/schemas/statistics/responses.ts similarity index 56% rename from src/domain/schemas/statistics.ts rename to src/domain/schemas/statistics/responses.ts index 1a35938..0d196e3 100644 --- a/src/domain/schemas/statistics.ts +++ b/src/domain/schemas/statistics/responses.ts @@ -1,17 +1,13 @@ import { z } from 'zod'; import { BRAZILIAN_STATES } from '@/constants/brazilian-states'; +import { REFERRAL_CATEGORIES } from '@/domain/enums/referrals'; -import { baseResponseSchema } from './base'; -import { GENDERS } from './patient'; -import { baseQuerySchema } from './query'; -import { REFERRAL_CATEGORIES } from './referral'; +import { baseResponseSchema } from '../base'; +import { GENDERS } from '../patient'; // Patients -export const PATIENTS_STATISTIC_FIELDS = ['gender', 'city', 'state'] as const; -export type PatientsStatisticField = (typeof PATIENTS_STATISTIC_FIELDS)[number]; - export const getTotalPatientsByStatusResponseSchema = baseResponseSchema.extend( { data: z.object({ @@ -25,24 +21,15 @@ export type GetTotalPatientsByStatusResponse = z.infer< typeof getTotalPatientsByStatusResponseSchema >; -export const getPatientsByPeriodQuerySchema = baseQuerySchema - .pick({ - period: true, - limit: true, - order: true, - withPercentage: true, - }) - .extend({ order: baseQuerySchema.shape.order.default('DESC') }); - -export const patientsByGenderSchema = z.object({ +export const totalPatientsByGenderSchema = z.object({ gender: z.enum(GENDERS), total: z.number(), }); -export type PatientsByGender = z.infer; +export type TotalPatientsByGender = z.infer; export const getPatientsByGenderResponseSchema = baseResponseSchema.extend({ data: z.object({ - genders: z.array(patientsByGenderSchema), + genders: z.array(totalPatientsByGenderSchema), total: z.number(), }), }); @@ -50,18 +37,18 @@ export type GetPatientsByGenderResponse = z.infer< typeof getPatientsByGenderResponseSchema >; -export const patientsByCitySchema = z +export const totalPatientsByCitySchema = z .object({ city: z.string(), total: z.number(), percentage: z.number(), }) .strict(); -export type PatientsByCity = z.infer; +export type TotalPatientsByCity = z.infer; export const getPatientsByCityResponseSchema = baseResponseSchema.extend({ data: z.object({ - cities: z.array(patientsByCitySchema), + cities: z.array(totalPatientsByCitySchema), total: z.number(), }), }); @@ -71,9 +58,6 @@ export type GetPatientsByCityResponse = z.infer< // Referrals -export const getTotalReferralsAndReferredPatientsPercentageQuerySchema = - baseQuerySchema.pick({ period: true }); - export const getTotalReferralsAndReferredPatientsPercentageResponseSchema = baseResponseSchema.extend({ data: z.object({ @@ -85,20 +69,18 @@ export type GetTotalReferralsAndReferredPatientsPercentageResponse = z.infer< typeof getTotalReferralsAndReferredPatientsPercentageResponseSchema >; -export const getReferredPatientsByStateQuerySchema = baseQuerySchema.pick({ - period: true, -}); - -export const stateReferredPatientsSchema = z.object({ +export const totalReferredPatientsByStateSchema = z.object({ state: z.enum(BRAZILIAN_STATES), total: z.number(), }); -export type StateReferredPatients = z.infer; +export type TotalReferredPatientsByStateSchema = z.infer< + typeof totalReferredPatientsByStateSchema +>; export const getReferredPatientsByStateResponseSchema = baseResponseSchema.extend({ data: z.object({ - states: z.array(stateReferredPatientsSchema), + states: z.array(totalReferredPatientsByStateSchema), total: z.number(), }), }); @@ -106,22 +88,18 @@ export type GetReferredPatientsByStateResponse = z.infer< typeof getReferredPatientsByStateResponseSchema >; -export const getTotalReferralsByCategoryQuerySchema = baseQuerySchema.pick({ - period: true, -}); - -export const categoryTotalReferralsSchema = z.object({ +export const totalReferralsByCategorySchema = z.object({ category: z.enum(REFERRAL_CATEGORIES), total: z.number(), }); -export type CategoryTotalReferrals = z.infer< - typeof categoryTotalReferralsSchema +export type TotalReferralsByCategory = z.infer< + typeof totalReferralsByCategorySchema >; export const getTotalReferralsByCategoryResponseSchema = baseResponseSchema.extend({ data: z.object({ - categories: z.array(categoryTotalReferralsSchema), + categories: z.array(totalReferralsByCategorySchema), total: z.number(), }), });