Skip to content
16 changes: 16 additions & 0 deletions infra/database/migrations/1765133594521-Update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class Update1765133594521 implements MigrationInterface {
name = 'Update1765133594521'

public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
await queryRunner.query(`ALTER TABLE \`referrals\` DROP COLUMN \`date\``);
await queryRunner.query(`ALTER TABLE \`referrals\` ADD \`date\` date NOT NULL`);
}

}
12 changes: 8 additions & 4 deletions infra/database/seed-dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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';

Expand Down Expand Up @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion src/app/http/patients/patients.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ import { PatientsService } from './patients.service';
],
controllers: [PatientsController],
providers: [PatientsService, PatientsRepository],
exports: [PatientsRepository],
exports: [PatientsRepository, TypeOrmModule.forFeature([Patient])],
})
export class PatientsModule {}
187 changes: 2 additions & 185 deletions src/app/http/patients/patients.repository.ts
Original file line number Diff line number Diff line change
@@ -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()
Expand Down Expand Up @@ -177,169 +159,4 @@ export class PatientsRepository {
public async deactivate(id: string): Promise<Patient> {
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<T>(
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<T>();

return { items, total };
}

public async getTotalPatients(
input: { status?: PatientStatus; startDate?: Date; endDate?: Date } = {},
): Promise<number> {
const { status, startDate, endDate } = input;

const where: FindOptionsWhere<Patient> = {
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<number> {
const { startDate, endDate } = input;

const where: FindOptionsWhere<Patient> = {
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<Patient> => {
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<Patient>,
) {
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<StateReferredPatients>(),
totalStatesQuery.getRawOne<{ total: string }>(),
]);

return {
states,
total: Number(totalResult?.total || 0),
};
}
}
49 changes: 41 additions & 8 deletions src/app/http/referrals/referrals.controller.ts
Original file line number Diff line number Diff line change
@@ -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<GetReferralsResponseSchema> {
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<BaseResponseSchema> {
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<BaseResponseSchema> {
await this.referralsService.cancel(id, user);
await this.cancelReferralUseCase.execute({ id, userId: user.id });

return {
success: true,
Expand Down
7 changes: 6 additions & 1 deletion src/app/http/referrals/referrals.dtos.ts
Original file line number Diff line number Diff line change
@@ -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) {}
19 changes: 13 additions & 6 deletions src/app/http/referrals/referrals.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
Loading