feature: a very minimal patients feature implemented. it needs lots of improvments though

This commit is contained in:
2026-04-29 13:32:22 +03:30
parent 1128fca81a
commit 6a3addd1ca
19 changed files with 929 additions and 3 deletions

View File

@@ -6,6 +6,7 @@ import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AdminModule } from './admin/admin.module';
import { PrismaModule } from '../prisma/prisma.module'; // ✅
import { PatientsModule } from './modules/patients/patients.module';
@Module({
imports: [
@@ -15,6 +16,7 @@ import { PrismaModule } from '../prisma/prisma.module'; // ✅
}),
PrismaModule, // ✅ ADD THIS
AuthModule,
PatientsModule,
AdminModule.forRoot(),
],
controllers: [AppController],

View File

@@ -31,6 +31,9 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
}
const { passwordHash, ...result } = user;
return result;
return {
...result,
organizationId: payload.organizationId,
};
}
}

View File

@@ -0,0 +1,29 @@
import { IsDateString, IsEmail, IsOptional, IsString, MaxLength } from 'class-validator';
export class CreatePatientDto {
@IsString()
@MaxLength(80)
firstName: string;
@IsString()
@MaxLength(80)
lastName: string;
@IsOptional()
@IsString()
@MaxLength(30)
phone?: string;
@IsOptional()
@IsEmail()
email?: string;
@IsOptional()
@IsDateString()
dateOfBirth?: string;
@IsOptional()
@IsString()
@MaxLength(1000)
notes?: string;
}

View File

@@ -0,0 +1,28 @@
import { IsDateString, IsNumber, IsOptional, IsString, MaxLength } from 'class-validator';
export class CreateTreatmentHistoryDto {
@IsString()
@MaxLength(120)
title: string;
@IsString()
@MaxLength(40)
status: string;
@IsDateString()
treatmentAt: string;
@IsOptional()
@IsString()
@MaxLength(20)
tooth?: string;
@IsOptional()
@IsString()
@MaxLength(1000)
notes?: string;
@IsOptional()
@IsNumber()
totalCost?: number;
}

View File

@@ -0,0 +1,21 @@
import { Transform } from 'class-transformer';
import { IsInt, IsOptional, IsString, Max, Min } from 'class-validator';
export class ListPatientsDto {
@IsOptional()
@IsString()
q?: string;
@IsOptional()
@Transform(({ value }) => Number(value))
@IsInt()
@Min(1)
page = 1;
@IsOptional()
@Transform(({ value }) => Number(value))
@IsInt()
@Min(1)
@Max(100)
limit = 10;
}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreatePatientDto } from './create-patient.dto';
export class UpdatePatientDto extends PartialType(CreatePatientDto) {}

View File

@@ -0,0 +1,77 @@
import {
Body,
Controller,
Get,
Param,
ParseIntPipe,
Patch,
Post,
Query,
Req,
UseGuards,
} from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { CreatePatientDto } from './dto/create-patient.dto';
import { ListPatientsDto } from './dto/list-patients.dto';
import { UpdatePatientDto } from './dto/update-patient.dto';
import { PatientsService } from './patients.service';
import { CreateTreatmentHistoryDto } from './dto/create-treatment-history.dto';
@ApiTags('patients')
@ApiBearerAuth('JWT-auth')
@UseGuards(JwtAuthGuard)
@Controller('patients')
export class PatientsController {
constructor(private readonly patientsService: PatientsService) {}
@Post()
@ApiOperation({ summary: 'Create a patient for current organization' })
create(@Body() createPatientDto: CreatePatientDto, @Req() req) {
const organizationId = this.patientsService.getOrganizationIdFromUser(req.user);
return this.patientsService.create(createPatientDto, organizationId);
}
@Get()
@ApiOperation({ summary: 'List patients with search and pagination' })
findAll(@Query() query: ListPatientsDto, @Req() req) {
const organizationId = this.patientsService.getOrganizationIdFromUser(req.user);
return this.patientsService.findAll(query, organizationId);
}
@Get(':id')
@ApiOperation({ summary: 'Get one patient by id' })
findOne(@Param('id') id: string, @Req() req) {
const organizationId = this.patientsService.getOrganizationIdFromUser(req.user);
return this.patientsService.findOne(id, organizationId);
}
@Patch(':id')
@ApiOperation({ summary: 'Update patient' })
update(@Param('id') id: string, @Body() updatePatientDto: UpdatePatientDto, @Req() req) {
const organizationId = this.patientsService.getOrganizationIdFromUser(req.user);
return this.patientsService.update(id, updatePatientDto, organizationId);
}
@Get(':id/treatments')
@ApiOperation({ summary: 'Get patient treatment history' })
findTreatments(
@Param('id') id: string,
@Query('limit', new ParseIntPipe({ optional: true })) limit = 20,
@Req() req,
) {
const organizationId = this.patientsService.getOrganizationIdFromUser(req.user);
return this.patientsService.findTreatments(id, organizationId, limit);
}
@Post(':id/treatments')
@ApiOperation({ summary: 'Add treatment history item for a patient' })
addTreatment(
@Param('id') id: string,
@Body() dto: CreateTreatmentHistoryDto,
@Req() req,
) {
const organizationId = this.patientsService.getOrganizationIdFromUser(req.user);
return this.patientsService.addTreatment(id, dto, organizationId);
}
}

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { PrismaService } from '../../../prisma/prisma.service';
import { PatientsController } from './patients.controller';
import { PatientsService } from './patients.service';
@Module({
controllers: [PatientsController],
providers: [PatientsService, PrismaService],
})
export class PatientsModule {}

View File

@@ -0,0 +1,140 @@
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../../prisma/prisma.service';
import { CreatePatientDto } from './dto/create-patient.dto';
import { ListPatientsDto } from './dto/list-patients.dto';
import { UpdatePatientDto } from './dto/update-patient.dto';
import { CreateTreatmentHistoryDto } from './dto/create-treatment-history.dto';
@Injectable()
export class PatientsService {
constructor(private readonly prisma: PrismaService) {}
async create(createPatientDto: CreatePatientDto, organizationId: string) {
const patient = await this.prisma.patient.create({
data: {
...createPatientDto,
dateOfBirth: createPatientDto.dateOfBirth ? new Date(createPatientDto.dateOfBirth) : null,
organizationId,
},
});
return { success: true, data: patient };
}
async findAll(query: ListPatientsDto, organizationId: string) {
const { page = 1, limit = 10, q } = query;
const skip = (page - 1) * limit;
const where: Prisma.PatientWhereInput = {
organizationId,
...(q
? {
OR: [
{ firstName: { contains: q, mode: 'insensitive' } },
{ lastName: { contains: q, mode: 'insensitive' } },
{ email: { contains: q, mode: 'insensitive' } },
{ phone: { contains: q, mode: 'insensitive' } },
],
}
: {}),
};
const [items, total] = await Promise.all([
this.prisma.patient.findMany({
where,
skip,
take: limit,
orderBy: [{ updatedAt: 'desc' }],
}),
this.prisma.patient.count({ where }),
]);
return {
success: true,
data: {
items,
pagination: {
page,
limit,
total,
totalPages: Math.max(1, Math.ceil(total / limit)),
},
},
};
}
async findOne(id: string, organizationId: string) {
const patient = await this.prisma.patient.findFirst({
where: { id, organizationId },
});
if (!patient) {
throw new NotFoundException('Patient not found');
}
return { success: true, data: patient };
}
async update(id: string, updatePatientDto: UpdatePatientDto, organizationId: string) {
await this.ensurePatient(id, organizationId);
const patient = await this.prisma.patient.update({
where: { id },
data: {
...updatePatientDto,
dateOfBirth: updatePatientDto.dateOfBirth ? new Date(updatePatientDto.dateOfBirth) : undefined,
},
});
return { success: true, data: patient };
}
async findTreatments(patientId: string, organizationId: string, limit = 20) {
await this.ensurePatient(patientId, organizationId);
const items = await this.prisma.patientTreatmentHistory.findMany({
where: { patientId },
orderBy: [{ treatmentAt: 'desc' }],
take: limit,
});
return { success: true, data: items };
}
async addTreatment(
patientId: string,
dto: CreateTreatmentHistoryDto,
organizationId: string,
) {
await this.ensurePatient(patientId, organizationId);
const treatment = await this.prisma.patientTreatmentHistory.create({
data: {
...dto,
treatmentAt: new Date(dto.treatmentAt),
patientId,
},
});
return { success: true, data: treatment };
}
private async ensurePatient(id: string, organizationId: string) {
const patient = await this.prisma.patient.findFirst({
where: { id, organizationId },
select: { id: true },
});
if (!patient) {
throw new NotFoundException('Patient not found');
}
}
getOrganizationIdFromUser(user: { organizationId?: string }) {
if (!user?.organizationId) {
throw new BadRequestException('Organization is not selected');
}
return user.organizationId;
}
}