diff --git a/src/app/http/patients/patients.repository.ts b/src/app/http/patients/patients.repository.ts index f02a6b2..538548f 100644 --- a/src/app/http/patients/patients.repository.ts +++ b/src/app/http/patients/patients.repository.ts @@ -1,15 +1,24 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { + Between, + type FindOptionsWhere, + IsNull, + LessThanOrEqual, + MoreThanOrEqual, + Not, + Repository, +} from 'typeorm'; import { Patient } from '@/domain/entities/patient'; -import type { PatientOrderByType, PatientType } from '@/domain/schemas/patient'; import type { - GetPatientsTotalResponseSchema, - PatientsStatisticFieldType, -} from '@/domain/schemas/statistics'; + PatientOrderBy, + PatientStatus, + PatientType, +} from '@/domain/schemas/patient'; +import type { PatientsStatisticField } from '@/domain/schemas/statistics'; -import type { GetPatientsByPeriodDto } from '../statistics/statistics.dtos'; +import type { GetPatientsByPeriodQuery } from '../statistics/statistics.dtos'; import { CreatePatientDto, FindAllPatientQueryDto } from './patients.dtos'; @Injectable() @@ -35,7 +44,7 @@ export class PatientsRepository { all, } = filters; - const ORDER_BY: Record = { + const ORDER_BY: Record = { name: 'user.name', email: 'user.email', status: 'patient.status', @@ -165,12 +174,15 @@ export class PatientsRepository { return this.patientsRepository.save({ id, status: 'inactive' }); } - public async getPatientsTotal(): Promise< - GetPatientsTotalResponseSchema['data'] - > { + public async getTotalPatientsByStatus(): Promise<{ + total: number; + active: number; + inactive: number; + }> { const raw = await this.patientsRepository .createQueryBuilder('patient') - .select('COUNT(*)', 'total') + .select('COUNT(patient.id)', 'total') + .where('patient.status != :status', { status: 'pending' }) .addSelect( `SUM(CASE WHEN patient.status = 'active' THEN 1 ELSE 0 END)`, 'active', @@ -189,10 +201,10 @@ export class PatientsRepository { } public async getPatientsStatisticsByPeriod( - field: PatientsStatisticFieldType, + field: PatientsStatisticField, startDate: Date, endDate: Date, - query: GetPatientsByPeriodDto, + query: GetPatientsByPeriodQuery, ): Promise<{ items: T[]; total: number }> { const totalQuery = this.patientsRepository .createQueryBuilder('patient') @@ -228,4 +240,59 @@ export class PatientsRepository { 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 }); + } } diff --git a/src/app/http/referrals/referrals.module.ts b/src/app/http/referrals/referrals.module.ts index 01388bf..77c9b8d 100644 --- a/src/app/http/referrals/referrals.module.ts +++ b/src/app/http/referrals/referrals.module.ts @@ -12,6 +12,6 @@ import { ReferralsService } from './referrals.service'; imports: [PatientsModule, TypeOrmModule.forFeature([Referral])], controllers: [ReferralsController], providers: [ReferralsService, ReferralsRepository], - exports: [ReferralsRepository], + exports: [ReferralsService, ReferralsRepository], }) export class ReferralsModule {} diff --git a/src/app/http/referrals/referrals.repository.ts b/src/app/http/referrals/referrals.repository.ts index e7fd060..420b1f0 100644 --- a/src/app/http/referrals/referrals.repository.ts +++ b/src/app/http/referrals/referrals.repository.ts @@ -1,9 +1,15 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { + Between, + type FindOptionsWhere, + LessThanOrEqual, + MoreThanOrEqual, + Repository, +} from 'typeorm'; import { Referral } from '@/domain/entities/referral'; -import { ReferralStatusType } from '@/domain/schemas/referral'; +import { ReferralStatus } from '@/domain/schemas/referral'; import { CreateReferralDto } from './referrals.dtos'; @@ -20,7 +26,7 @@ export class ReferralsRepository { public async create( createReferralDto: CreateReferralDto & { - status: ReferralStatusType; + status: ReferralStatus; referred_by: string; }, ): Promise { @@ -31,4 +37,34 @@ export class ReferralsRepository { 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 }); + } } diff --git a/src/app/http/statistics/statistics.controller.ts b/src/app/http/statistics/statistics.controller.ts index 4ef8a8e..390dba0 100644 --- a/src/app/http/statistics/statistics.controller.ts +++ b/src/app/http/statistics/statistics.controller.ts @@ -5,11 +5,15 @@ import { Roles } from '@/common/decorators/roles.decorator'; import type { GetPatientsByCityResponse, GetPatientsByGenderResponse, - PatientsByCityType, - PatientsByGenderType, + GetTotalReferralsAndReferredPatientsPercentageResponse, + PatientsByCity, + PatientsByGender, } from '@/domain/schemas/statistics'; -import { GetPatientsByPeriodDto } from './statistics.dtos'; +import { + GetPatientsByPeriodQuery, + GetTotalReferralsAndReferredPatientsPercentageQuery, +} from './statistics.dtos'; import { StatisticsService } from './statistics.service'; @ApiTags('Estatísticas') @@ -34,10 +38,10 @@ export class StatisticsController { @Roles(['manager', 'nurse']) @ApiOperation({ summary: 'Estatísticas de pacientes por gênero' }) async getPatientsByGender( - @Query() query: GetPatientsByPeriodDto, + @Query() query: GetPatientsByPeriodQuery, ): Promise { const { items: genders, total } = - await this.statisticsService.getPatientsByPeriod( + await this.statisticsService.getPatientsByPeriod( 'gender', query, ); @@ -53,10 +57,10 @@ export class StatisticsController { @Roles(['manager', 'nurse']) @ApiOperation({ summary: 'Estatísticas de pacientes por cidade' }) async getPatientsByCity( - @Query() query: GetPatientsByPeriodDto, + @Query() query: GetPatientsByPeriodQuery, ): Promise { const { items: cities, total } = - await this.statisticsService.getPatientsByPeriod( + await this.statisticsService.getPatientsByPeriod( 'city', query, ); @@ -67,4 +71,23 @@ export class StatisticsController { data: { cities, total }, }; } + + @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( + query, + ); + + return { + success: true, + message: + 'Estatísticas com total de encaminhamentos retornada com sucesso.', + data, + }; + } } diff --git a/src/app/http/statistics/statistics.dtos.ts b/src/app/http/statistics/statistics.dtos.ts index c66b083..5f87abe 100644 --- a/src/app/http/statistics/statistics.dtos.ts +++ b/src/app/http/statistics/statistics.dtos.ts @@ -1,7 +1,14 @@ import { createZodDto } from 'nestjs-zod'; -import { getPatientsByPeriodSchema } from '@/domain/schemas/statistics'; +import { + getPatientsByPeriodSchema, + getTotalReferralsAndReferredPatientsPercentageQuerySchema, +} from '@/domain/schemas/statistics'; -export class GetPatientsByPeriodDto extends createZodDto( +export class GetPatientsByPeriodQuery extends createZodDto( getPatientsByPeriodSchema, ) {} + +export class GetTotalReferralsAndReferredPatientsPercentageQuery extends createZodDto( + getTotalReferralsAndReferredPatientsPercentageQuerySchema, +) {} diff --git a/src/app/http/statistics/statistics.module.ts b/src/app/http/statistics/statistics.module.ts index 2ee9061..2c9cd33 100644 --- a/src/app/http/statistics/statistics.module.ts +++ b/src/app/http/statistics/statistics.module.ts @@ -3,11 +3,12 @@ import { Module } from '@nestjs/common'; 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'; @Module({ - imports: [PatientsModule, UtilsModule], + imports: [PatientsModule, UtilsModule, ReferralsModule], controllers: [StatisticsController], providers: [StatisticsService], }) diff --git a/src/app/http/statistics/statistics.service.ts b/src/app/http/statistics/statistics.service.ts index 4798dff..85d1a9e 100644 --- a/src/app/http/statistics/statistics.service.ts +++ b/src/app/http/statistics/statistics.service.ts @@ -1,28 +1,33 @@ import { Injectable } from '@nestjs/common'; import type { - GetPatientsTotalResponseSchema, - PatientsStatisticFieldType, + GetTotalPatientsByStatusResponse, + PatientsStatisticField, } from '@/domain/schemas/statistics'; import { UtilsService } from '@/utils/utils.service'; import { PatientsRepository } from '../patients/patients.repository'; -import type { GetPatientsByPeriodDto } from './statistics.dtos'; +import { ReferralsRepository } from '../referrals/referrals.repository'; +import type { + GetPatientsByPeriodQuery, + GetTotalReferralsAndReferredPatientsPercentageQuery, +} 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.getPatientsTotal(); + async getPatientsTotal(): Promise { + return await this.patientsRepository.getTotalPatientsByStatus(); } async getPatientsByPeriod( - filter: PatientsStatisticFieldType, - query: GetPatientsByPeriodDto, + filter: PatientsStatisticField, + query: GetPatientsByPeriodQuery, ): Promise<{ items: T[]; total: number }> { const { startDate, endDate } = this.utilsService.getDateRangeForPeriod( query.period, @@ -35,4 +40,30 @@ export class StatisticsService { 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)), + }; + } } diff --git a/src/domain/entities/patient.ts b/src/domain/entities/patient.ts index 5a6ef6b..9bbe613 100644 --- a/src/domain/entities/patient.ts +++ b/src/domain/entities/patient.ts @@ -16,11 +16,11 @@ import { import { User } from '@/domain/entities/user'; import { + Gender, GENDERS, - GenderType, PATIENT_STATUS, PatientSchema, - PatientStatusType, + PatientStatus, } from '../schemas/patient'; import { Appointment } from './appointment'; import { PatientRequirement } from './patient-requirement'; @@ -36,7 +36,7 @@ export class Patient implements PatientSchema { user_id: string; @Column({ type: 'enum', enum: GENDERS }) - gender: GenderType; + gender: Gender; @Column({ type: 'date' }) date_of_birth: Date; @@ -72,7 +72,7 @@ export class Patient implements PatientSchema { has_nmo_diagnosis: boolean; @Column({ type: 'enum', enum: PATIENT_STATUS, default: 'pending' }) - status: PatientStatusType; + status: PatientStatus; @CreateDateColumn({ type: 'timestamp' }) created_at: Date; diff --git a/src/domain/entities/referral.ts b/src/domain/entities/referral.ts index 3860d9d..d8ea942 100644 --- a/src/domain/entities/referral.ts +++ b/src/domain/entities/referral.ts @@ -8,13 +8,13 @@ import { UpdateDateColumn, } from 'typeorm'; -import { PATIENT_CONDITIONS, PatientConditionType } from '../schemas/patient'; +import { PATIENT_CONDITIONS, PatientCondition } from '../schemas/patient'; import { REFERRAL_CATEGORIES, REFERRAL_STATUSES, - ReferralCategoryType, + ReferralCategory, ReferralSchema, - ReferralStatusType, + ReferralStatus, } from '../schemas/referral'; import { Patient } from './patient'; @@ -30,13 +30,13 @@ export class Referral implements ReferralSchema { date: Date; @Column({ type: 'enum', enum: REFERRAL_CATEGORIES }) - category: ReferralCategoryType; + category: ReferralCategory; @Column({ type: 'enum', enum: PATIENT_CONDITIONS }) - condition: PatientConditionType; + condition: PatientCondition; @Column({ type: 'enum', enum: REFERRAL_STATUSES, default: 'scheduled' }) - status: ReferralStatusType; + status: ReferralStatus; @Column({ type: 'varchar', length: 500, nullable: true }) annotation: string | null; diff --git a/src/domain/schemas/patient.ts b/src/domain/schemas/patient.ts index 82fa2da..3c8c53a 100644 --- a/src/domain/schemas/patient.ts +++ b/src/domain/schemas/patient.ts @@ -17,22 +17,22 @@ export const GENDERS = [ 'non_binary', 'prefer_not_to_say', ] as const; -export type GenderType = (typeof GENDERS)[number]; +export type Gender = (typeof GENDERS)[number]; export const PATIENT_STATUS = ['active', 'inactive', 'pending'] as const; -export type PatientStatusType = (typeof PATIENT_STATUS)[number]; +export type PatientStatus = (typeof PATIENT_STATUS)[number]; export const PATIENT_ORDER_BY = ['name', 'email', 'status', 'date'] as const; -export type PatientOrderByType = (typeof PATIENT_ORDER_BY)[number]; +export type PatientOrderBy = (typeof PATIENT_ORDER_BY)[number]; export const PATIENT_STATISTICS = ['gender', 'total'] as const; export type PatientStatisticsResult = { - gender: GenderType; + gender: Gender; total: number; }; export const PATIENT_CONDITIONS = ['in_crisis', 'stable'] as const; -export type PatientConditionType = (typeof PATIENT_CONDITIONS)[number]; +export type PatientCondition = (typeof PATIENT_CONDITIONS)[number]; export const patientSchema = z .object({ diff --git a/src/domain/schemas/query.ts b/src/domain/schemas/query.ts index 9619ae3..6c096cd 100644 --- a/src/domain/schemas/query.ts +++ b/src/domain/schemas/query.ts @@ -3,13 +3,18 @@ import { z } from 'zod'; export const ORDER = ['ASC', 'DESC'] as const; export type OrderType = (typeof ORDER)[number]; -export const PERIOD = ['last-year', 'last-month', 'last-week'] as const; +export const PERIOD = [ + 'today', + 'last-year', + 'last-month', + 'last-week', +] as const; export type PeriodType = (typeof PERIOD)[number]; export const baseQuerySchema = z.object({ search: z.string().optional(), order: z.enum(ORDER).optional(), - period: z.enum(PERIOD).optional().default('last-week'), + period: z.enum(PERIOD).optional().default('today'), page: z.coerce.number().min(1).optional().default(1), perPage: z.coerce.number().min(1).max(50).optional().default(10), limit: z.coerce.number().min(1).optional().default(10), diff --git a/src/domain/schemas/referral.ts b/src/domain/schemas/referral.ts index d327962..ac9aeca 100644 --- a/src/domain/schemas/referral.ts +++ b/src/domain/schemas/referral.ts @@ -8,7 +8,7 @@ export const REFERRAL_STATUSES = [ 'completed', 'no_show', ] as const; -export type ReferralStatusType = (typeof REFERRAL_STATUSES)[number]; +export type ReferralStatus = (typeof REFERRAL_STATUSES)[number]; export const REFERRAL_CATEGORIES = [ 'medical_care', @@ -22,7 +22,7 @@ export const REFERRAL_CATEGORIES = [ 'neurology', 'ophthalmology', ] as const; -export type ReferralCategoryType = (typeof REFERRAL_CATEGORIES)[number]; +export type ReferralCategory = (typeof REFERRAL_CATEGORIES)[number]; export const referralSchema = z .object({ diff --git a/src/domain/schemas/statistics.ts b/src/domain/schemas/statistics.ts index d9473c4..39860b5 100644 --- a/src/domain/schemas/statistics.ts +++ b/src/domain/schemas/statistics.ts @@ -7,18 +7,19 @@ import { baseQuerySchema } from './query'; // Patients export const PATIENTS_STATISTIC_FIELDS = ['gender', 'city'] as const; -export type PatientsStatisticFieldType = - (typeof PATIENTS_STATISTIC_FIELDS)[number]; +export type PatientsStatisticField = (typeof PATIENTS_STATISTIC_FIELDS)[number]; -export const getPatientsTotalResponseSchema = baseResponseSchema.extend({ - data: z.object({ - total: z.number(), - active: z.number(), - inactive: z.number(), - }), -}); -export type GetPatientsTotalResponseSchema = z.infer< - typeof getPatientsTotalResponseSchema +export const getTotalPatientsByStatusResponseSchema = baseResponseSchema.extend( + { + data: z.object({ + total: z.number(), + active: z.number(), + inactive: z.number(), + }), + }, +); +export type GetTotalPatientsByStatusResponse = z.infer< + typeof getTotalPatientsByStatusResponseSchema >; export const getPatientsByPeriodSchema = baseQuerySchema @@ -34,7 +35,7 @@ export const patientsByGenderSchema = z.object({ gender: z.enum(GENDERS), total: z.number(), }); -export type PatientsByGenderType = z.infer; +export type PatientsByGender = z.infer; export const getPatientsByGenderResponseSchema = baseResponseSchema.extend({ data: z.object({ @@ -53,7 +54,7 @@ export const patientsByCitySchema = z percentage: z.number(), }) .strict(); -export type PatientsByCityType = z.infer; +export type PatientsByCity = z.infer; export const getPatientsByCityResponseSchema = baseResponseSchema.extend({ data: z.object({ @@ -64,3 +65,19 @@ export const getPatientsByCityResponseSchema = baseResponseSchema.extend({ export type GetPatientsByCityResponse = z.infer< typeof getPatientsByCityResponseSchema >; + +export const getTotalReferralsAndReferredPatientsPercentageQuerySchema = + baseQuerySchema.pick({ + period: true, + }); + +export const getTotalReferralsAndReferredPatientsPercentageResponseSchema = + baseResponseSchema.extend({ + data: z.object({ + totalReferrals: z.number(), + referredPatientsPercentage: z.number(), + }), + }); +export type GetTotalReferralsAndReferredPatientsPercentageResponse = z.infer< + typeof getTotalReferralsAndReferredPatientsPercentageResponseSchema +>; diff --git a/src/utils/utils.service.ts b/src/utils/utils.service.ts index 6fac3cd..c3acadc 100644 --- a/src/utils/utils.service.ts +++ b/src/utils/utils.service.ts @@ -1,5 +1,11 @@ import { Injectable } from '@nestjs/common'; -import { startOfMonth, startOfWeek, startOfYear } from 'date-fns'; +import { + endOfDay, + startOfDay, + startOfMonth, + startOfWeek, + startOfYear, +} from 'date-fns'; import { type CookieOptions, Response } from 'express'; import type { PeriodType } from '@/domain/schemas/query'; @@ -57,17 +63,21 @@ export class UtilsService { const today = new Date(); const periodMapper = { + today: { + startDate: startOfDay(today), + endDate: endOfDay(today), + }, 'last-week': { startDate: startOfWeek(today), - endDate: today, + endDate: endOfDay(today), }, 'last-month': { startDate: startOfMonth(today), - endDate: today, + endDate: endOfDay(today), }, 'last-year': { startDate: startOfYear(today), - endDate: today, + endDate: endOfDay(today), }, };