diff --git a/src/app/http/patients/patients.repository.ts b/src/app/http/patients/patients.repository.ts index 538548f..fffcd60 100644 --- a/src/app/http/patients/patients.repository.ts +++ b/src/app/http/patients/patients.repository.ts @@ -8,6 +8,7 @@ import { MoreThanOrEqual, Not, Repository, + type SelectQueryBuilder, } from 'typeorm'; import { Patient } from '@/domain/entities/patient'; @@ -16,7 +17,10 @@ import type { PatientStatus, PatientType, } from '@/domain/schemas/patient'; -import type { PatientsStatisticField } from '@/domain/schemas/statistics'; +import type { + PatientsStatisticField, + StateReferredPatients, +} from '@/domain/schemas/statistics'; import type { GetPatientsByPeriodQuery } from '../statistics/statistics.dtos'; import { CreatePatientDto, FindAllPatientQueryDto } from './patients.dtos'; @@ -179,7 +183,7 @@ export class PatientsRepository { active: number; inactive: number; }> { - const raw = await this.patientsRepository + const queryBuilder = await this.patientsRepository .createQueryBuilder('patient') .select('COUNT(patient.id)', 'total') .where('patient.status != :status', { status: 'pending' }) @@ -194,9 +198,9 @@ export class PatientsRepository { .getRawOne<{ total: string; active: string; inactive: string }>(); return { - total: Number(raw?.total ?? 0), - active: Number(raw?.active ?? 0), - inactive: Number(raw?.inactive ?? 0), + total: Number(queryBuilder?.total ?? 0), + active: Number(queryBuilder?.active ?? 0), + inactive: Number(queryBuilder?.inactive ?? 0), }; } @@ -220,7 +224,7 @@ export class PatientsRepository { const queryBuilder = this.patientsRepository .createQueryBuilder('patient') .select(`patient.${field}`, field) - .addSelect('COUNT(*)', 'total') + .addSelect('COUNT(patient.id)', 'total') .where('patient.created_at BETWEEN :start AND :end', { start: startDate, end: endDate, @@ -242,11 +246,7 @@ export class PatientsRepository { } public async getTotalPatients( - input: { - status?: PatientStatus; - startDate?: Date; - endDate?: Date; - } = {}, + input: { status?: PatientStatus; startDate?: Date; endDate?: Date } = {}, ): Promise { const { status, startDate, endDate } = input; @@ -270,10 +270,7 @@ export class PatientsRepository { } public async getTotalReferredPatients( - input: { - startDate?: Date; - endDate?: Date; - } = {}, + input: { startDate?: Date; endDate?: Date } = {}, ): Promise { const { startDate, endDate } = input; @@ -295,4 +292,54 @@ export class PatientsRepository { 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/statistics/statistics.controller.ts b/src/app/http/statistics/statistics.controller.ts index 390dba0..b0aae11 100644 --- a/src/app/http/statistics/statistics.controller.ts +++ b/src/app/http/statistics/statistics.controller.ts @@ -5,6 +5,7 @@ import { Roles } from '@/common/decorators/roles.decorator'; import type { GetPatientsByCityResponse, GetPatientsByGenderResponse, + GetReferredPatientsByStateResponse, GetTotalReferralsAndReferredPatientsPercentageResponse, PatientsByCity, PatientsByGender, @@ -12,6 +13,7 @@ import type { import { GetPatientsByPeriodQuery, + GetReferredPatientsByStateQuery, GetTotalReferralsAndReferredPatientsPercentageQuery, } from './statistics.dtos'; import { StatisticsService } from './statistics.service'; @@ -72,7 +74,7 @@ export class StatisticsController { }; } - @Get('referrals/total') + @Get('referrals-total') @Roles(['manager', 'nurse']) @ApiOperation({ summary: 'Estatísticas do total de encaminhamentos' }) async getTotalReferralsAndReferredPatientsPercentage( @@ -90,4 +92,23 @@ export class StatisticsController { data, }; } + + @Get('referrals-by-state') + @Roles(['manager', 'nurse']) + @ApiOperation({ + summary: 'Lista com o total de pacientes encaminhados por estado', + }) + async getReferredPatientsByState( + @Query() query: GetReferredPatientsByStateQuery, + ): Promise { + const { states, total } = + await this.statisticsService.getReferredPatientsByState(query); + + return { + success: true, + message: + 'Lista com o total de pacientes encaminhados por estado retornada com sucesso.', + data: { states, total }, + }; + } } diff --git a/src/app/http/statistics/statistics.dtos.ts b/src/app/http/statistics/statistics.dtos.ts index 5f87abe..bd9861e 100644 --- a/src/app/http/statistics/statistics.dtos.ts +++ b/src/app/http/statistics/statistics.dtos.ts @@ -1,14 +1,19 @@ import { createZodDto } from 'nestjs-zod'; import { - getPatientsByPeriodSchema, + getPatientsByPeriodQuerySchema, + getReferredPatientsByStateQuerySchema, getTotalReferralsAndReferredPatientsPercentageQuerySchema, } from '@/domain/schemas/statistics'; export class GetPatientsByPeriodQuery extends createZodDto( - getPatientsByPeriodSchema, + getPatientsByPeriodQuerySchema, ) {} export class GetTotalReferralsAndReferredPatientsPercentageQuery extends createZodDto( getTotalReferralsAndReferredPatientsPercentageQuerySchema, ) {} + +export class GetReferredPatientsByStateQuery extends createZodDto( + getReferredPatientsByStateQuerySchema, +) {} diff --git a/src/app/http/statistics/statistics.service.ts b/src/app/http/statistics/statistics.service.ts index 85d1a9e..bf63d73 100644 --- a/src/app/http/statistics/statistics.service.ts +++ b/src/app/http/statistics/statistics.service.ts @@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common'; import type { GetTotalPatientsByStatusResponse, PatientsStatisticField, + StateReferredPatients, } from '@/domain/schemas/statistics'; import { UtilsService } from '@/utils/utils.service'; @@ -10,6 +11,7 @@ import { PatientsRepository } from '../patients/patients.repository'; import { ReferralsRepository } from '../referrals/referrals.repository'; import type { GetPatientsByPeriodQuery, + GetReferredPatientsByStateQuery, GetTotalReferralsAndReferredPatientsPercentageQuery, } from './statistics.dtos'; @@ -66,4 +68,17 @@ export class StatisticsService { referredPatientsPercentage: Number(percentage.toFixed(2)), }; } + + 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/constants/brazilian-states.ts b/src/constants/brazilian-states.ts index e91674b..e4b7f1e 100644 --- a/src/constants/brazilian-states.ts +++ b/src/constants/brazilian-states.ts @@ -27,4 +27,4 @@ export const BRAZILIAN_STATES = [ 'SE', 'TO', ] as const; -export type BrazilianStateType = (typeof BRAZILIAN_STATES)[number]; +export type BrazilianState = (typeof BRAZILIAN_STATES)[number]; diff --git a/src/domain/entities/patient.ts b/src/domain/entities/patient.ts index 9bbe613..590e7cd 100644 --- a/src/domain/entities/patient.ts +++ b/src/domain/entities/patient.ts @@ -11,7 +11,7 @@ import { import { BRAZILIAN_STATES, - type BrazilianStateType, + type BrazilianState, } from '@/constants/brazilian-states'; import { User } from '@/domain/entities/user'; @@ -48,7 +48,7 @@ export class Patient implements PatientSchema { cpf: string; @Column({ type: 'enum', enum: BRAZILIAN_STATES }) - state: BrazilianStateType; + state: BrazilianState; @Column({ type: 'varchar', length: 50 }) city: string; diff --git a/src/domain/schemas/query.ts b/src/domain/schemas/query.ts index 6c096cd..92d7ad8 100644 --- a/src/domain/schemas/query.ts +++ b/src/domain/schemas/query.ts @@ -1,20 +1,20 @@ import { z } from 'zod'; -export const ORDER = ['ASC', 'DESC'] as const; -export type OrderType = (typeof ORDER)[number]; +export const QUERY_ORDER = ['ASC', 'DESC'] as const; +export type QueryOrder = (typeof QUERY_ORDER)[number]; -export const PERIOD = [ +export const QUERY_PERIOD = [ 'today', 'last-year', 'last-month', 'last-week', ] as const; -export type PeriodType = (typeof PERIOD)[number]; +export type QueryPeriod = (typeof QUERY_PERIOD)[number]; export const baseQuerySchema = z.object({ search: z.string().optional(), - order: z.enum(ORDER).optional(), - period: z.enum(PERIOD).optional().default('today'), + order: z.enum(QUERY_ORDER).optional(), + period: z.enum(QUERY_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/statistics.ts b/src/domain/schemas/statistics.ts index 39860b5..f8e4348 100644 --- a/src/domain/schemas/statistics.ts +++ b/src/domain/schemas/statistics.ts @@ -1,12 +1,14 @@ import { z } from 'zod'; +import { BRAZILIAN_STATES } from '@/constants/brazilian-states'; + import { baseResponseSchema } from './base'; import { GENDERS } from './patient'; import { baseQuerySchema } from './query'; // Patients -export const PATIENTS_STATISTIC_FIELDS = ['gender', 'city'] as const; +export const PATIENTS_STATISTIC_FIELDS = ['gender', 'city', 'state'] as const; export type PatientsStatisticField = (typeof PATIENTS_STATISTIC_FIELDS)[number]; export const getTotalPatientsByStatusResponseSchema = baseResponseSchema.extend( @@ -22,7 +24,7 @@ export type GetTotalPatientsByStatusResponse = z.infer< typeof getTotalPatientsByStatusResponseSchema >; -export const getPatientsByPeriodSchema = baseQuerySchema +export const getPatientsByPeriodQuerySchema = baseQuerySchema .pick({ period: true, limit: true, @@ -66,10 +68,10 @@ export type GetPatientsByCityResponse = z.infer< typeof getPatientsByCityResponseSchema >; +// Referrals + export const getTotalReferralsAndReferredPatientsPercentageQuerySchema = - baseQuerySchema.pick({ - period: true, - }); + baseQuerySchema.pick({ period: true }); export const getTotalReferralsAndReferredPatientsPercentageResponseSchema = baseResponseSchema.extend({ @@ -81,3 +83,24 @@ export const getTotalReferralsAndReferredPatientsPercentageResponseSchema = export type GetTotalReferralsAndReferredPatientsPercentageResponse = z.infer< typeof getTotalReferralsAndReferredPatientsPercentageResponseSchema >; + +export const getReferredPatientsByStateQuerySchema = baseQuerySchema.pick({ + period: true, +}); + +export const stateReferredPatientsSchema = z.object({ + state: z.enum(BRAZILIAN_STATES), + total: z.number(), +}); +export type StateReferredPatients = z.infer; + +export const getReferredPatientsByStateResponseSchema = + baseResponseSchema.extend({ + data: z.object({ + states: z.array(stateReferredPatientsSchema), + total: z.number(), + }), + }); +export type GetReferredPatientsByStateResponse = z.infer< + typeof getReferredPatientsByStateResponseSchema +>; diff --git a/src/utils/utils.service.ts b/src/utils/utils.service.ts index c3acadc..a3ce020 100644 --- a/src/utils/utils.service.ts +++ b/src/utils/utils.service.ts @@ -8,7 +8,7 @@ import { } from 'date-fns'; import { type CookieOptions, Response } from 'express'; -import type { PeriodType } from '@/domain/schemas/query'; +import type { QueryPeriod } from '@/domain/schemas/query'; import { EnvService } from '@/env/env.service'; type SetCookieOptions = CookieOptions & { @@ -56,7 +56,7 @@ export class UtilsService { }); } - getDateRangeForPeriod(period: PeriodType): { + getDateRangeForPeriod(period: QueryPeriod): { startDate: Date; endDate: Date; } {