From 5899f854488193fca83cbb102ac2e140389241aa Mon Sep 17 00:00:00 2001 From: Paulo Roberto Date: Thu, 4 Dec 2025 00:35:01 -0300 Subject: [PATCH 1/9] feat(query): change default period from 'last-week' to 'today' --- src/domain/schemas/query.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/domain/schemas/query.ts b/src/domain/schemas/query.ts index 9619ae3..20eb039 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 = [ + 'last-year', + 'last-month', + 'last-week', + 'today', +] 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), From 140b82e96ecb2cf2f0069c996b5cb0f9f5736f74 Mon Sep 17 00:00:00 2001 From: Paulo Roberto Date: Thu, 4 Dec 2025 00:41:10 -0300 Subject: [PATCH 2/9] feat(statistics): add referrals total query and response schemas --- src/domain/schemas/statistics.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/domain/schemas/statistics.ts b/src/domain/schemas/statistics.ts index d9473c4..bc8a6e9 100644 --- a/src/domain/schemas/statistics.ts +++ b/src/domain/schemas/statistics.ts @@ -64,3 +64,17 @@ export const getPatientsByCityResponseSchema = baseResponseSchema.extend({ export type GetPatientsByCityResponse = z.infer< typeof getPatientsByCityResponseSchema >; + +export const getReferralsTotalSchema = baseQuerySchema.pick({ + period: true, +}); + +export const getReferralsTotalResponseSchema = baseResponseSchema.extend({ + data: z.object({ + total: z.number(), + percentage: z.number(), + }), +}); +export type GetReferralsTotalResponseSchema = z.infer< + typeof getReferralsTotalResponseSchema +>; From 3e3211380aed9cac988210478d1b95a878c673a3 Mon Sep 17 00:00:00 2001 From: Paulo Roberto Date: Thu, 4 Dec 2025 00:42:35 -0300 Subject: [PATCH 3/9] feat(statistics): add GetReferralsTotal DTO --- src/app/http/statistics/statistics.dtos.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/app/http/statistics/statistics.dtos.ts b/src/app/http/statistics/statistics.dtos.ts index c66b083..4978f78 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, + getReferralsTotalSchema, +} from '@/domain/schemas/statistics'; export class GetPatientsByPeriodDto extends createZodDto( getPatientsByPeriodSchema, ) {} + +export class GetReferralsTotalDto extends createZodDto( + getReferralsTotalSchema, +) {} From fb1ef7415a2099263945034221b1415125fe8754 Mon Sep 17 00:00:00 2001 From: Paulo Roberto Date: Thu, 4 Dec 2025 00:46:54 -0300 Subject: [PATCH 4/9] chore(referrals): update ReferralsModule dependencies --- src/app/http/referrals/referrals.module.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/app/http/referrals/referrals.module.ts b/src/app/http/referrals/referrals.module.ts index c4a0119..5ce2ebf 100644 --- a/src/app/http/referrals/referrals.module.ts +++ b/src/app/http/referrals/referrals.module.ts @@ -1,10 +1,17 @@ import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +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'; @Module({ + imports: [TypeOrmModule.forFeature([Referral]), PatientsModule], controllers: [ReferralsController], - providers: [ReferralsService], + providers: [ReferralsService, ReferralsRepository], + exports: [ReferralsRepository, ReferralsService], }) export class ReferralsModule {} From c90b5d82488873e532c59e018adfbe85fed953ae Mon Sep 17 00:00:00 2001 From: Paulo Roberto Date: Thu, 4 Dec 2025 00:48:53 -0300 Subject: [PATCH 5/9] chore(statistics): add ReferralsModule imports --- src/app/http/statistics/statistics.module.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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], }) From cee549a96d121fca019de61c94a2dbefb39c43bc Mon Sep 17 00:00:00 2001 From: Paulo Roberto Date: Thu, 4 Dec 2025 00:50:44 -0300 Subject: [PATCH 6/9] feat(referrals): add referrals statistics calculation - Implement ReferralsRepository.getReferralsTotal() method - Add period filtering (last-year, last-month, last-week, today) - Calculate percentage of non-pending patients with referrals - Use raw SQL for date filtering in MySQL queries - Return { total: number, percentage: number } response --- .../http/referrals/referrals.repository.ts | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 src/app/http/referrals/referrals.repository.ts diff --git a/src/app/http/referrals/referrals.repository.ts b/src/app/http/referrals/referrals.repository.ts new file mode 100644 index 0000000..20153e5 --- /dev/null +++ b/src/app/http/referrals/referrals.repository.ts @@ -0,0 +1,89 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, SelectQueryBuilder } from 'typeorm'; + +import { Referral } from '@/domain/entities/referral'; +import { GetReferralsTotalResponseSchema } from '@/domain/schemas/statistics'; + +import { PatientsRepository } from '../patients/patients.repository'; +import { GetReferralsTotalDto } from '../statistics/statistics.dtos'; + +@Injectable() +export class ReferralsRepository { + constructor( + @InjectRepository(Referral) + private readonly referralsRepository: Repository, + private readonly patientsRepository: PatientsRepository, + ) {} + + async getReferralsTotal( + filters: GetReferralsTotalDto, + ): Promise { + const { period } = filters; + + const queryReferralsTotal = this.referralsRepository + .createQueryBuilder('referral') + .leftJoinAndSelect('referral.patient', 'patient') + .where("patient.status <> 'pending'"); + + const queryPatientsNonPending = this.referralsRepository + .createQueryBuilder('referral') + .leftJoin('referral.patient', 'patient') + .select('COUNT(DISTINCT patient.id)', 'count') + .where("patient.status <> 'pending'"); + + this.applyPeriodFilter(queryReferralsTotal, period); + this.applyPeriodFilter(queryPatientsNonPending, period); + + const [totalReferrals, uniquePatientsResult] = await Promise.all([ + queryReferralsTotal.getCount(), + queryPatientsNonPending.getRawOne<{ count: string }>(), + ]); + + const patientsPerReferrals = uniquePatientsResult + ? Number(uniquePatientsResult.count) + : 0; + + const totalPatients = (await this.patientsRepository.getPatientsTotal()) + .total; + + const percentage = Math.round((patientsPerReferrals / totalPatients) * 100); + + return { + total: totalReferrals, + percentage: percentage, + }; + } + + private applyPeriodFilter( + queryBuilder: SelectQueryBuilder, + period: string, + ): void { + if (period === 'last-year') { + queryBuilder + .andWhere( + "referral.date >= DATE_FORMAT(CURRENT_DATE - INTERVAL 1 YEAR, '%Y-01-01')", + ) + .andWhere("referral.date < DATE_FORMAT(CURRENT_DATE, '%Y-01-01')"); + } + if (period === 'last-month') { + queryBuilder + .andWhere( + "referral.date >= DATE_FORMAT(CURRENT_DATE, '%Y-%m-01') - INTERVAL 1 MONTH", + ) + .andWhere("referral.date < DATE_FORMAT(CURRENT_DATE, '%Y-%m-01')"); + } + if (period === 'last-week') { + queryBuilder + .andWhere( + 'referral.date >= DATE_SUB(DATE_SUB(CURRENT_DATE, INTERVAL WEEKDAY(CURRENT_DATE) DAY), INTERVAL 1 WEEK)', + ) + .andWhere( + 'referral.date < DATE_SUB(CURRENT_DATE, INTERVAL WEEKDAY(CURRENT_DATE) DAY)', + ); + } + if (period === 'today') { + queryBuilder.andWhere('referral.date = CURDATE()'); + } + } +} From 5507355072154422fdb639869ab4f51d88e60771 Mon Sep 17 00:00:00 2001 From: Paulo Roberto Date: Thu, 4 Dec 2025 00:52:42 -0300 Subject: [PATCH 7/9] feat(referrals): add referrals total service method - Implement getReferralsTotal in ReferralsService - Delegate logic to referrals repository - Maintain consistent DTO and response types --- src/app/http/statistics/statistics.service.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/app/http/statistics/statistics.service.ts b/src/app/http/statistics/statistics.service.ts index 4798dff..acb0b97 100644 --- a/src/app/http/statistics/statistics.service.ts +++ b/src/app/http/statistics/statistics.service.ts @@ -2,18 +2,24 @@ import { Injectable } from '@nestjs/common'; import type { GetPatientsTotalResponseSchema, + GetReferralsTotalResponseSchema, PatientsStatisticFieldType, } 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 { + GetPatientsByPeriodDto, + GetReferralsTotalDto, +} from './statistics.dtos'; @Injectable() export class StatisticsService { constructor( private readonly patientsRepository: PatientsRepository, private readonly utilsService: UtilsService, + private readonly referralsRepository: ReferralsRepository, ) {} async getPatientsTotal(): Promise { @@ -35,4 +41,10 @@ export class StatisticsService { query, ); } + + async getReferralsTotal( + filters: GetReferralsTotalDto, + ): Promise { + return await this.referralsRepository.getReferralsTotal(filters); + } } From ae984e4bc906109adb62ba8a718baa09d0074859 Mon Sep 17 00:00:00 2001 From: Paulo Roberto Date: Thu, 4 Dec 2025 00:54:27 -0300 Subject: [PATCH 8/9] feat(referrals): implement referrals statistics API endpoint - Create controller endpoint for referrals total statistics - Apply role-based authorization (manager, nurse, admin) - Integrate service layer and Zod validation - Follow consistent API response pattern --- .../http/statistics/statistics.controller.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/app/http/statistics/statistics.controller.ts b/src/app/http/statistics/statistics.controller.ts index 4ef8a8e..23225ea 100644 --- a/src/app/http/statistics/statistics.controller.ts +++ b/src/app/http/statistics/statistics.controller.ts @@ -9,7 +9,10 @@ import type { PatientsByGenderType, } from '@/domain/schemas/statistics'; -import { GetPatientsByPeriodDto } from './statistics.dtos'; +import { + GetPatientsByPeriodDto, + GetReferralsTotalDto, +} from './statistics.dtos'; import { StatisticsService } from './statistics.service'; @ApiTags('Estatísticas') @@ -67,4 +70,17 @@ export class StatisticsController { data: { cities, total }, }; } + + @Get('referrals/total') + @Roles(['manager', 'nurse', 'admin']) + @ApiOperation({ summary: 'Estatísticas totais de encaminhamentos' }) + async getReferralsTotal(@Query() filters: GetReferralsTotalDto) { + const data = await this.statisticsService.getReferralsTotal(filters); + return { + success: true, + message: + 'Estatísticas com total de encaminhamentos retornada com sucesso.', + data, + }; + } } From bab8d931824d13b48afbe18043112ee778ff9b54 Mon Sep 17 00:00:00 2001 From: Juliano Sill Date: Sat, 6 Dec 2025 17:04:31 -0300 Subject: [PATCH 9/9] fix(statistics): exclude pending patients from total patients by status --- src/app/http/patients/patients.repository.ts | 22 +++++++-------- .../http/statistics/statistics.controller.ts | 14 +++++----- src/app/http/statistics/statistics.dtos.ts | 2 +- src/app/http/statistics/statistics.service.ts | 19 +++++++------ src/domain/schemas/statistics.ts | 27 ++++++++++--------- 5 files changed, 42 insertions(+), 42 deletions(-) diff --git a/src/app/http/patients/patients.repository.ts b/src/app/http/patients/patients.repository.ts index cd08d26..538548f 100644 --- a/src/app/http/patients/patients.repository.ts +++ b/src/app/http/patients/patients.repository.ts @@ -16,12 +16,9 @@ import type { PatientStatus, PatientType, } from '@/domain/schemas/patient'; -import type { - GetPatientsTotalResponseSchema, - PatientsStatisticFieldType, -} from '@/domain/schemas/statistics'; +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() @@ -177,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', @@ -201,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') diff --git a/src/app/http/statistics/statistics.controller.ts b/src/app/http/statistics/statistics.controller.ts index bc9b0e4..390dba0 100644 --- a/src/app/http/statistics/statistics.controller.ts +++ b/src/app/http/statistics/statistics.controller.ts @@ -6,12 +6,12 @@ import type { GetPatientsByCityResponse, GetPatientsByGenderResponse, GetTotalReferralsAndReferredPatientsPercentageResponse, - PatientsByCityType, - PatientsByGenderType, + PatientsByCity, + PatientsByGender, } from '@/domain/schemas/statistics'; import { - GetPatientsByPeriodDto, + GetPatientsByPeriodQuery, GetTotalReferralsAndReferredPatientsPercentageQuery, } from './statistics.dtos'; import { StatisticsService } from './statistics.service'; @@ -38,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, ); @@ -57,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, ); diff --git a/src/app/http/statistics/statistics.dtos.ts b/src/app/http/statistics/statistics.dtos.ts index 351fb06..5f87abe 100644 --- a/src/app/http/statistics/statistics.dtos.ts +++ b/src/app/http/statistics/statistics.dtos.ts @@ -5,7 +5,7 @@ import { getTotalReferralsAndReferredPatientsPercentageQuerySchema, } from '@/domain/schemas/statistics'; -export class GetPatientsByPeriodDto extends createZodDto( +export class GetPatientsByPeriodQuery extends createZodDto( getPatientsByPeriodSchema, ) {} diff --git a/src/app/http/statistics/statistics.service.ts b/src/app/http/statistics/statistics.service.ts index 42fffca..85d1a9e 100644 --- a/src/app/http/statistics/statistics.service.ts +++ b/src/app/http/statistics/statistics.service.ts @@ -1,15 +1,15 @@ 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 { ReferralsRepository } from '../referrals/referrals.repository'; import type { - GetPatientsByPeriodDto, + GetPatientsByPeriodQuery, GetTotalReferralsAndReferredPatientsPercentageQuery, } from './statistics.dtos'; @@ -21,13 +21,13 @@ export class StatisticsService { 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, @@ -58,9 +58,8 @@ export class StatisticsService { }), ]); - console.log([totalPatients, totalReferrals, totalReferredPatients]); - - const percentage = Number((totalReferredPatients / totalPatients) * 100); + const percentage = + Number((totalReferredPatients / totalPatients) * 100) || 0; return { totalReferrals, diff --git a/src/domain/schemas/statistics.ts b/src/domain/schemas/statistics.ts index c2df217..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({