Migración desde erp-clinicas/backend - Estándar multi-repo v2
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
3f4b0c21b6
commit
f8aacbd316
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
dist/
|
||||
coverage/
|
||||
.env
|
||||
195
src/app.integration.ts
Normal file
195
src/app.integration.ts
Normal file
@ -0,0 +1,195 @@
|
||||
/**
|
||||
* Application Integration - ERP Clinicas
|
||||
*
|
||||
* Integrates all clinical modules and configures the application
|
||||
*/
|
||||
|
||||
import express, { Express } from 'express';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
// Import modules
|
||||
import { PatientsModule } from './modules/patients';
|
||||
import { AppointmentsModule } from './modules/appointments';
|
||||
import { ConsultationsModule } from './modules/consultations';
|
||||
|
||||
// Import entities from all modules for TypeORM
|
||||
import { Patient } from './modules/patients/entities';
|
||||
import { Specialty, Doctor, Appointment, AppointmentSlot } from './modules/appointments/entities';
|
||||
import {
|
||||
Consultation,
|
||||
Diagnosis,
|
||||
Prescription,
|
||||
PrescriptionItem,
|
||||
VitalSignsRecord,
|
||||
} from './modules/consultations/entities';
|
||||
|
||||
/**
|
||||
* Get all entities for TypeORM configuration
|
||||
*/
|
||||
export function getAllEntities() {
|
||||
return [
|
||||
// Patients (CL-002)
|
||||
Patient,
|
||||
// Appointments (CL-003)
|
||||
Specialty,
|
||||
Doctor,
|
||||
Appointment,
|
||||
AppointmentSlot,
|
||||
// Consultations (CL-004)
|
||||
Consultation,
|
||||
Diagnosis,
|
||||
Prescription,
|
||||
PrescriptionItem,
|
||||
VitalSignsRecord,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Module configuration options
|
||||
*/
|
||||
export interface ModuleOptions {
|
||||
patients?: { enabled: boolean; basePath?: string };
|
||||
appointments?: { enabled: boolean; basePath?: string };
|
||||
consultations?: { enabled: boolean; basePath?: string };
|
||||
}
|
||||
|
||||
/**
|
||||
* Default module options
|
||||
*/
|
||||
const defaultModuleOptions: ModuleOptions = {
|
||||
patients: { enabled: true, basePath: '/api' },
|
||||
appointments: { enabled: true, basePath: '/api' },
|
||||
consultations: { enabled: true, basePath: '/api' },
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize and integrate all modules
|
||||
*/
|
||||
export function initializeModules(
|
||||
app: Express,
|
||||
dataSource: DataSource,
|
||||
options: ModuleOptions = {}
|
||||
): void {
|
||||
const config = { ...defaultModuleOptions, ...options };
|
||||
|
||||
// Initialize Patients Module
|
||||
if (config.patients?.enabled) {
|
||||
const patientsModule = new PatientsModule({
|
||||
dataSource,
|
||||
basePath: config.patients.basePath,
|
||||
});
|
||||
app.use(patientsModule.router);
|
||||
console.log('✅ Patients module initialized (CL-002)');
|
||||
}
|
||||
|
||||
// Initialize Appointments Module
|
||||
if (config.appointments?.enabled) {
|
||||
const appointmentsModule = new AppointmentsModule({
|
||||
dataSource,
|
||||
basePath: config.appointments.basePath,
|
||||
});
|
||||
app.use(appointmentsModule.router);
|
||||
console.log('✅ Appointments module initialized (CL-003)');
|
||||
}
|
||||
|
||||
// Initialize Consultations Module
|
||||
if (config.consultations?.enabled) {
|
||||
const consultationsModule = new ConsultationsModule({
|
||||
dataSource,
|
||||
basePath: config.consultations.basePath,
|
||||
});
|
||||
app.use(consultationsModule.router);
|
||||
console.log('✅ Consultations module initialized (CL-004)');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create TypeORM DataSource configuration
|
||||
*/
|
||||
export function createDataSourceConfig(options: {
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password: string;
|
||||
database: string;
|
||||
ssl?: boolean;
|
||||
logging?: boolean;
|
||||
}) {
|
||||
return {
|
||||
type: 'postgres' as const,
|
||||
host: options.host,
|
||||
port: options.port,
|
||||
username: options.username,
|
||||
password: options.password,
|
||||
database: options.database,
|
||||
ssl: options.ssl ? { rejectUnauthorized: false } : false,
|
||||
logging: options.logging ?? false,
|
||||
entities: getAllEntities(),
|
||||
synchronize: false, // Use migrations instead
|
||||
migrations: ['src/migrations/*.ts'],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Example application setup
|
||||
*/
|
||||
export async function createApplication(dataSourceConfig: any): Promise<Express> {
|
||||
// Create Express app
|
||||
const app = express();
|
||||
|
||||
// Middleware
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// CORS middleware
|
||||
app.use((req, res, next) => {
|
||||
res.header('Access-Control-Allow-Origin', '*');
|
||||
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
|
||||
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Tenant-ID, X-User-ID');
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.sendStatus(200);
|
||||
return;
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// Initialize database
|
||||
const dataSource = new DataSource(dataSourceConfig);
|
||||
await dataSource.initialize();
|
||||
console.log('✅ Database connected');
|
||||
|
||||
// Initialize all modules
|
||||
initializeModules(app, dataSource);
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'healthy',
|
||||
timestamp: new Date().toISOString(),
|
||||
project: 'erp-clinicas',
|
||||
modules: {
|
||||
patients: true,
|
||||
appointments: true,
|
||||
consultations: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Error handling middleware
|
||||
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
console.error('Error:', err);
|
||||
res.status(err.status || 500).json({
|
||||
error: err.message || 'Internal Server Error',
|
||||
code: err.code || 'INTERNAL_ERROR',
|
||||
});
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
export default {
|
||||
getAllEntities,
|
||||
initializeModules,
|
||||
createDataSourceConfig,
|
||||
createApplication,
|
||||
};
|
||||
19
src/modules/appointments/appointments.module.ts
Normal file
19
src/modules/appointments/appointments.module.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Router } from 'express';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { AppointmentController } from './controllers';
|
||||
|
||||
export interface AppointmentsModuleOptions {
|
||||
dataSource: DataSource;
|
||||
basePath?: string;
|
||||
}
|
||||
|
||||
export class AppointmentsModule {
|
||||
public router: Router;
|
||||
private controller: AppointmentController;
|
||||
|
||||
constructor(options: AppointmentsModuleOptions) {
|
||||
const { dataSource, basePath = '/api' } = options;
|
||||
this.controller = new AppointmentController(dataSource, basePath);
|
||||
this.router = this.controller.router;
|
||||
}
|
||||
}
|
||||
288
src/modules/appointments/controllers/index.ts
Normal file
288
src/modules/appointments/controllers/index.ts
Normal file
@ -0,0 +1,288 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { SpecialtyService, DoctorService, AppointmentService, AppointmentSlotService } from '../services';
|
||||
import {
|
||||
CreateSpecialtyDto, UpdateSpecialtyDto,
|
||||
CreateDoctorDto, UpdateDoctorDto,
|
||||
CreateAppointmentDto, UpdateAppointmentDto, AppointmentQueryDto,
|
||||
ConfirmAppointmentDto, CancelAppointmentDto,
|
||||
CreateAppointmentSlotDto, AvailableSlotsQueryDto,
|
||||
} from '../dto';
|
||||
|
||||
export class AppointmentController {
|
||||
public router: Router;
|
||||
private specialtyService: SpecialtyService;
|
||||
private doctorService: DoctorService;
|
||||
private appointmentService: AppointmentService;
|
||||
private slotService: AppointmentSlotService;
|
||||
|
||||
constructor(dataSource: DataSource, basePath: string = '/api') {
|
||||
this.router = Router();
|
||||
this.specialtyService = new SpecialtyService(dataSource);
|
||||
this.doctorService = new DoctorService(dataSource);
|
||||
this.appointmentService = new AppointmentService(dataSource);
|
||||
this.slotService = new AppointmentSlotService(dataSource);
|
||||
this.setupRoutes(basePath);
|
||||
}
|
||||
|
||||
private setupRoutes(basePath: string): void {
|
||||
// Specialties
|
||||
this.router.get(`${basePath}/specialties`, this.getSpecialties.bind(this));
|
||||
this.router.get(`${basePath}/specialties/:id`, this.getSpecialtyById.bind(this));
|
||||
this.router.post(`${basePath}/specialties`, this.createSpecialty.bind(this));
|
||||
this.router.patch(`${basePath}/specialties/:id`, this.updateSpecialty.bind(this));
|
||||
|
||||
// Doctors
|
||||
this.router.get(`${basePath}/doctors`, this.getDoctors.bind(this));
|
||||
this.router.get(`${basePath}/doctors/:id`, this.getDoctorById.bind(this));
|
||||
this.router.get(`${basePath}/doctors/:id/schedule`, this.getDoctorSchedule.bind(this));
|
||||
this.router.post(`${basePath}/doctors`, this.createDoctor.bind(this));
|
||||
this.router.patch(`${basePath}/doctors/:id`, this.updateDoctor.bind(this));
|
||||
this.router.delete(`${basePath}/doctors/:id`, this.deleteDoctor.bind(this));
|
||||
|
||||
// Appointments
|
||||
this.router.get(`${basePath}/appointments`, this.getAppointments.bind(this));
|
||||
this.router.get(`${basePath}/appointments/available-slots`, this.getAvailableSlots.bind(this));
|
||||
this.router.get(`${basePath}/appointments/:id`, this.getAppointmentById.bind(this));
|
||||
this.router.post(`${basePath}/appointments`, this.createAppointment.bind(this));
|
||||
this.router.patch(`${basePath}/appointments/:id`, this.updateAppointment.bind(this));
|
||||
this.router.post(`${basePath}/appointments/:id/confirm`, this.confirmAppointment.bind(this));
|
||||
this.router.post(`${basePath}/appointments/:id/cancel`, this.cancelAppointment.bind(this));
|
||||
this.router.post(`${basePath}/appointments/:id/check-in`, this.checkIn.bind(this));
|
||||
this.router.post(`${basePath}/appointments/:id/check-out`, this.checkOut.bind(this));
|
||||
|
||||
// Appointment Slots
|
||||
this.router.post(`${basePath}/appointment-slots`, this.createSlot.bind(this));
|
||||
this.router.delete(`${basePath}/appointment-slots/:id`, this.deleteSlot.bind(this));
|
||||
}
|
||||
|
||||
private getTenantId(req: Request): string {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) throw new Error('x-tenant-id header is required');
|
||||
return tenantId;
|
||||
}
|
||||
|
||||
private getUserId(req: Request): string | undefined {
|
||||
return req.headers['x-user-id'] as string;
|
||||
}
|
||||
|
||||
// Specialties
|
||||
private async getSpecialties(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const data = await this.specialtyService.findAll(tenantId);
|
||||
res.json({ data });
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
|
||||
private async getSpecialtyById(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const { id } = req.params;
|
||||
const data = await this.specialtyService.findById(tenantId, id);
|
||||
if (!data) { res.status(404).json({ error: 'Specialty not found' }); return; }
|
||||
res.json({ data });
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
|
||||
private async createSpecialty(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const dto: CreateSpecialtyDto = req.body;
|
||||
const data = await this.specialtyService.create(tenantId, dto);
|
||||
res.status(201).json({ data });
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
|
||||
private async updateSpecialty(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const { id } = req.params;
|
||||
const dto: UpdateSpecialtyDto = req.body;
|
||||
const data = await this.specialtyService.update(tenantId, id, dto);
|
||||
if (!data) { res.status(404).json({ error: 'Specialty not found' }); return; }
|
||||
res.json({ data });
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
|
||||
// Doctors
|
||||
private async getDoctors(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const specialtyId = req.query.specialtyId as string;
|
||||
const data = await this.doctorService.findAll(tenantId, specialtyId);
|
||||
res.json({ data });
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
|
||||
private async getDoctorById(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const { id } = req.params;
|
||||
const data = await this.doctorService.findById(tenantId, id);
|
||||
if (!data) { res.status(404).json({ error: 'Doctor not found' }); return; }
|
||||
res.json({ data });
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
|
||||
private async getDoctorSchedule(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const { id } = req.params;
|
||||
const slots = await this.slotService.findByDoctor(tenantId, id);
|
||||
res.json({ data: slots });
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
|
||||
private async createDoctor(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const dto: CreateDoctorDto = req.body;
|
||||
const data = await this.doctorService.create(tenantId, dto);
|
||||
res.status(201).json({ data });
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
|
||||
private async updateDoctor(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const { id } = req.params;
|
||||
const dto: UpdateDoctorDto = req.body;
|
||||
const data = await this.doctorService.update(tenantId, id, dto);
|
||||
if (!data) { res.status(404).json({ error: 'Doctor not found' }); return; }
|
||||
res.json({ data });
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
|
||||
private async deleteDoctor(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const { id } = req.params;
|
||||
const deleted = await this.doctorService.softDelete(tenantId, id);
|
||||
if (!deleted) { res.status(404).json({ error: 'Doctor not found' }); return; }
|
||||
res.status(204).send();
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
|
||||
// Appointments
|
||||
private async getAppointments(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const query: AppointmentQueryDto = {
|
||||
patientId: req.query.patientId as string,
|
||||
doctorId: req.query.doctorId as string,
|
||||
dateFrom: req.query.dateFrom as string,
|
||||
dateTo: req.query.dateTo as string,
|
||||
status: req.query.status as any,
|
||||
page: req.query.page ? parseInt(req.query.page as string) : 1,
|
||||
limit: req.query.limit ? parseInt(req.query.limit as string) : 20,
|
||||
};
|
||||
const result = await this.appointmentService.findAll(tenantId, query);
|
||||
res.json({ data: result.data, meta: { total: result.total, page: query.page, limit: query.limit } });
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
|
||||
private async getAvailableSlots(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const doctorId = req.query.doctorId as string;
|
||||
const date = req.query.date as string;
|
||||
if (!doctorId || !date) { res.status(400).json({ error: 'doctorId and date are required' }); return; }
|
||||
const slots = await this.appointmentService.getAvailableSlots(tenantId, doctorId, date);
|
||||
res.json({ data: slots });
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
|
||||
private async getAppointmentById(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const { id } = req.params;
|
||||
const data = await this.appointmentService.findById(tenantId, id);
|
||||
if (!data) { res.status(404).json({ error: 'Appointment not found' }); return; }
|
||||
res.json({ data });
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
|
||||
private async createAppointment(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const userId = this.getUserId(req);
|
||||
const dto: CreateAppointmentDto = req.body;
|
||||
const data = await this.appointmentService.create(tenantId, dto, userId);
|
||||
res.status(201).json({ data });
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
|
||||
private async updateAppointment(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const { id } = req.params;
|
||||
const dto: UpdateAppointmentDto = req.body;
|
||||
const data = await this.appointmentService.update(tenantId, id, dto);
|
||||
if (!data) { res.status(404).json({ error: 'Appointment not found' }); return; }
|
||||
res.json({ data });
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
|
||||
private async confirmAppointment(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const { id } = req.params;
|
||||
const { confirmedBy }: ConfirmAppointmentDto = req.body;
|
||||
const data = await this.appointmentService.confirm(tenantId, id, confirmedBy);
|
||||
if (!data) { res.status(404).json({ error: 'Appointment not found' }); return; }
|
||||
res.json({ data });
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
|
||||
private async cancelAppointment(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const { id } = req.params;
|
||||
const { cancelledBy, cancellationReason }: CancelAppointmentDto = req.body;
|
||||
const data = await this.appointmentService.cancel(tenantId, id, cancelledBy, cancellationReason);
|
||||
if (!data) { res.status(404).json({ error: 'Appointment not found' }); return; }
|
||||
res.json({ data });
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
|
||||
private async checkIn(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const { id } = req.params;
|
||||
const data = await this.appointmentService.checkIn(tenantId, id);
|
||||
if (!data) { res.status(404).json({ error: 'Appointment not found' }); return; }
|
||||
res.json({ data });
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
|
||||
private async checkOut(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const { id } = req.params;
|
||||
const data = await this.appointmentService.checkOut(tenantId, id);
|
||||
if (!data) { res.status(404).json({ error: 'Appointment not found' }); return; }
|
||||
res.json({ data });
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
|
||||
// Appointment Slots
|
||||
private async createSlot(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const dto: CreateAppointmentSlotDto = req.body;
|
||||
const data = await this.slotService.create(tenantId, dto);
|
||||
res.status(201).json({ data });
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
|
||||
private async deleteSlot(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const { id } = req.params;
|
||||
const deleted = await this.slotService.delete(tenantId, id);
|
||||
if (!deleted) { res.status(404).json({ error: 'Slot not found' }); return; }
|
||||
res.status(204).send();
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
}
|
||||
286
src/modules/appointments/dto/index.ts
Normal file
286
src/modules/appointments/dto/index.ts
Normal file
@ -0,0 +1,286 @@
|
||||
import { IsString, IsOptional, IsUUID, IsEnum, IsBoolean, IsDateString, IsInt, Min, Max, MaxLength } from 'class-validator';
|
||||
import { AppointmentStatus } from '../entities';
|
||||
|
||||
// Specialty DTOs
|
||||
export class CreateSpecialtyDto {
|
||||
@IsString()
|
||||
@MaxLength(20)
|
||||
code: string;
|
||||
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
name: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
requiresReferral?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(15)
|
||||
@Max(120)
|
||||
consultationDurationMinutes?: number;
|
||||
}
|
||||
|
||||
export class UpdateSpecialtyDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
requiresReferral?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(15)
|
||||
@Max(120)
|
||||
consultationDurationMinutes?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
// Doctor DTOs
|
||||
export class CreateDoctorDto {
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
firstName: string;
|
||||
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
lastName: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(255)
|
||||
email?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(20)
|
||||
phone?: string;
|
||||
|
||||
@IsString()
|
||||
@MaxLength(50)
|
||||
professionalLicense: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(50)
|
||||
specialtyLicense?: string;
|
||||
|
||||
@IsUUID()
|
||||
specialtyId: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(15)
|
||||
@Max(120)
|
||||
consultationDurationMinutes?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(50)
|
||||
maxAppointmentsPerDay?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
acceptsInsurance?: boolean;
|
||||
}
|
||||
|
||||
export class UpdateDoctorDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
firstName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
lastName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(255)
|
||||
email?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(20)
|
||||
phone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(50)
|
||||
specialtyLicense?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
specialtyId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(15)
|
||||
@Max(120)
|
||||
consultationDurationMinutes?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(50)
|
||||
maxAppointmentsPerDay?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
acceptsInsurance?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
// Appointment DTOs
|
||||
export class CreateAppointmentDto {
|
||||
@IsUUID()
|
||||
patientId: string;
|
||||
|
||||
@IsUUID()
|
||||
doctorId: string;
|
||||
|
||||
@IsDateString()
|
||||
scheduledDate: string;
|
||||
|
||||
@IsString()
|
||||
scheduledTime: string; // "HH:mm" format
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(15)
|
||||
@Max(180)
|
||||
durationMinutes?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
reason?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export class UpdateAppointmentDto {
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
scheduledDate?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
scheduledTime?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(15)
|
||||
@Max(180)
|
||||
durationMinutes?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
reason?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export class ConfirmAppointmentDto {
|
||||
@IsUUID()
|
||||
confirmedBy: string;
|
||||
}
|
||||
|
||||
export class CancelAppointmentDto {
|
||||
@IsUUID()
|
||||
cancelledBy: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
cancellationReason?: string;
|
||||
}
|
||||
|
||||
export class AppointmentQueryDto {
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
patientId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
doctorId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
dateFrom?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
dateTo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(['scheduled', 'confirmed', 'in_progress', 'completed', 'cancelled', 'no_show'])
|
||||
status?: AppointmentStatus;
|
||||
|
||||
@IsOptional()
|
||||
page?: number;
|
||||
|
||||
@IsOptional()
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
// Appointment Slot DTOs
|
||||
export class CreateAppointmentSlotDto {
|
||||
@IsUUID()
|
||||
doctorId: string;
|
||||
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@Max(6)
|
||||
dayOfWeek: number;
|
||||
|
||||
@IsString()
|
||||
startTime: string; // "HH:mm"
|
||||
|
||||
@IsString()
|
||||
endTime: string; // "HH:mm"
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(15)
|
||||
@Max(120)
|
||||
slotDurationMinutes?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
validFrom?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
validUntil?: string;
|
||||
}
|
||||
|
||||
export class AvailableSlotsQueryDto {
|
||||
@IsUUID()
|
||||
doctorId: string;
|
||||
|
||||
@IsDateString()
|
||||
date: string;
|
||||
}
|
||||
59
src/modules/appointments/entities/appointment-slot.entity.ts
Normal file
59
src/modules/appointments/entities/appointment-slot.entity.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
Index,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { Doctor } from './doctor.entity';
|
||||
|
||||
@Entity({ name: 'appointment_slots', schema: 'clinica' })
|
||||
export class AppointmentSlot {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||
tenantId: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'doctor_id', type: 'uuid' })
|
||||
doctorId: string;
|
||||
|
||||
@ManyToOne(() => Doctor)
|
||||
@JoinColumn({ name: 'doctor_id' })
|
||||
doctor: Doctor;
|
||||
|
||||
// Horario (0=domingo, 1=lunes, ..., 6=sabado)
|
||||
@Index()
|
||||
@Column({ name: 'day_of_week', type: 'integer' })
|
||||
dayOfWeek: number;
|
||||
|
||||
@Column({ name: 'start_time', type: 'time' })
|
||||
startTime: string;
|
||||
|
||||
@Column({ name: 'end_time', type: 'time' })
|
||||
endTime: string;
|
||||
|
||||
@Column({ name: 'slot_duration_minutes', type: 'integer', default: 30 })
|
||||
slotDurationMinutes: number;
|
||||
|
||||
// Control
|
||||
@Column({ type: 'boolean', default: true })
|
||||
active: boolean;
|
||||
|
||||
@Column({ name: 'valid_from', type: 'date', default: () => 'CURRENT_DATE' })
|
||||
validFrom: Date;
|
||||
|
||||
@Column({ name: 'valid_until', type: 'date', nullable: true })
|
||||
validUntil?: Date;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
93
src/modules/appointments/entities/appointment.entity.ts
Normal file
93
src/modules/appointments/entities/appointment.entity.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
Index,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { Doctor } from './doctor.entity';
|
||||
|
||||
export type AppointmentStatus = 'scheduled' | 'confirmed' | 'in_progress' | 'completed' | 'cancelled' | 'no_show';
|
||||
|
||||
@Entity({ name: 'appointments', schema: 'clinica' })
|
||||
export class Appointment {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||
tenantId: string;
|
||||
|
||||
// Relaciones
|
||||
@Index()
|
||||
@Column({ name: 'patient_id', type: 'uuid' })
|
||||
patientId: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'doctor_id', type: 'uuid' })
|
||||
doctorId: string;
|
||||
|
||||
@ManyToOne(() => Doctor)
|
||||
@JoinColumn({ name: 'doctor_id' })
|
||||
doctor: Doctor;
|
||||
|
||||
// Programacion
|
||||
@Index()
|
||||
@Column({ name: 'scheduled_date', type: 'date' })
|
||||
scheduledDate: Date;
|
||||
|
||||
@Column({ name: 'scheduled_time', type: 'time' })
|
||||
scheduledTime: string;
|
||||
|
||||
@Column({ name: 'duration_minutes', type: 'integer', default: 30 })
|
||||
durationMinutes: number;
|
||||
|
||||
// Estado
|
||||
@Index()
|
||||
@Column({ type: 'enum', enum: ['scheduled', 'confirmed', 'in_progress', 'completed', 'cancelled', 'no_show'], default: 'scheduled' })
|
||||
status: AppointmentStatus;
|
||||
|
||||
// Detalles
|
||||
@Column({ type: 'text', nullable: true })
|
||||
reason?: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
notes?: string;
|
||||
|
||||
// Confirmacion
|
||||
@Column({ name: 'confirmed_at', type: 'timestamptz', nullable: true })
|
||||
confirmedAt?: Date;
|
||||
|
||||
@Column({ name: 'confirmed_by', type: 'uuid', nullable: true })
|
||||
confirmedBy?: string;
|
||||
|
||||
// Cancelacion
|
||||
@Column({ name: 'cancelled_at', type: 'timestamptz', nullable: true })
|
||||
cancelledAt?: Date;
|
||||
|
||||
@Column({ name: 'cancelled_by', type: 'uuid', nullable: true })
|
||||
cancelledBy?: string;
|
||||
|
||||
@Column({ name: 'cancellation_reason', type: 'text', nullable: true })
|
||||
cancellationReason?: string;
|
||||
|
||||
// Check-in/Check-out
|
||||
@Column({ name: 'check_in_at', type: 'timestamptz', nullable: true })
|
||||
checkInAt?: Date;
|
||||
|
||||
@Column({ name: 'check_out_at', type: 'timestamptz', nullable: true })
|
||||
checkOutAt?: Date;
|
||||
|
||||
// Control
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
|
||||
@Column({ name: 'created_by', type: 'uuid', nullable: true })
|
||||
createdBy?: string;
|
||||
}
|
||||
80
src/modules/appointments/entities/doctor.entity.ts
Normal file
80
src/modules/appointments/entities/doctor.entity.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Index,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { Specialty } from './specialty.entity';
|
||||
|
||||
@Entity({ name: 'doctors', schema: 'clinica' })
|
||||
export class Doctor {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||
tenantId: string;
|
||||
|
||||
@Column({ name: 'employee_id', type: 'uuid', nullable: true })
|
||||
employeeId?: string;
|
||||
|
||||
// Datos personales
|
||||
@Column({ name: 'first_name', type: 'varchar', length: 100 })
|
||||
firstName: string;
|
||||
|
||||
@Column({ name: 'last_name', type: 'varchar', length: 100 })
|
||||
lastName: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||
email?: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 20, nullable: true })
|
||||
phone?: string;
|
||||
|
||||
// Datos profesionales
|
||||
@Index()
|
||||
@Column({ name: 'professional_license', type: 'varchar', length: 50 })
|
||||
professionalLicense: string;
|
||||
|
||||
@Column({ name: 'specialty_license', type: 'varchar', length: 50, nullable: true })
|
||||
specialtyLicense?: string;
|
||||
|
||||
@Column({ name: 'specialty_id', type: 'uuid' })
|
||||
specialtyId: string;
|
||||
|
||||
@ManyToOne(() => Specialty)
|
||||
@JoinColumn({ name: 'specialty_id' })
|
||||
specialty: Specialty;
|
||||
|
||||
// Configuracion
|
||||
@Column({ name: 'consultation_duration_minutes', type: 'integer', default: 30 })
|
||||
consultationDurationMinutes: number;
|
||||
|
||||
@Column({ name: 'max_appointments_per_day', type: 'integer', default: 20 })
|
||||
maxAppointmentsPerDay: number;
|
||||
|
||||
@Column({ name: 'accepts_insurance', type: 'boolean', default: true })
|
||||
acceptsInsurance: boolean;
|
||||
|
||||
// Control
|
||||
@Column({ type: 'boolean', default: true })
|
||||
active: boolean;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
|
||||
@DeleteDateColumn({ name: 'deleted_at', type: 'timestamptz', nullable: true })
|
||||
deletedAt?: Date;
|
||||
|
||||
get fullName(): string {
|
||||
return `Dr. ${this.firstName} ${this.lastName}`;
|
||||
}
|
||||
}
|
||||
4
src/modules/appointments/entities/index.ts
Normal file
4
src/modules/appointments/entities/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export { Specialty } from './specialty.entity';
|
||||
export { Doctor } from './doctor.entity';
|
||||
export { Appointment, AppointmentStatus } from './appointment.entity';
|
||||
export { AppointmentSlot } from './appointment-slot.entity';
|
||||
43
src/modules/appointments/entities/specialty.entity.ts
Normal file
43
src/modules/appointments/entities/specialty.entity.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
Index,
|
||||
OneToMany,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity({ name: 'specialties', schema: 'clinica' })
|
||||
export class Specialty {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||
tenantId: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 20 })
|
||||
code: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 100 })
|
||||
name: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description?: string;
|
||||
|
||||
@Column({ name: 'requires_referral', type: 'boolean', default: false })
|
||||
requiresReferral: boolean;
|
||||
|
||||
@Column({ name: 'consultation_duration_minutes', type: 'integer', default: 30 })
|
||||
consultationDurationMinutes: number;
|
||||
|
||||
@Column({ type: 'boolean', default: true })
|
||||
active: boolean;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
5
src/modules/appointments/index.ts
Normal file
5
src/modules/appointments/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export { AppointmentsModule, AppointmentsModuleOptions } from './appointments.module';
|
||||
export { Specialty, Doctor, Appointment, AppointmentStatus, AppointmentSlot } from './entities';
|
||||
export { SpecialtyService, DoctorService, AppointmentService, AppointmentSlotService } from './services';
|
||||
export { AppointmentController } from './controllers';
|
||||
export * from './dto';
|
||||
291
src/modules/appointments/services/index.ts
Normal file
291
src/modules/appointments/services/index.ts
Normal file
@ -0,0 +1,291 @@
|
||||
import { DataSource, Repository, Between, LessThanOrEqual, MoreThanOrEqual } from 'typeorm';
|
||||
import { Specialty, Doctor, Appointment, AppointmentSlot } from '../entities';
|
||||
import {
|
||||
CreateSpecialtyDto, UpdateSpecialtyDto,
|
||||
CreateDoctorDto, UpdateDoctorDto,
|
||||
CreateAppointmentDto, UpdateAppointmentDto, AppointmentQueryDto,
|
||||
CreateAppointmentSlotDto, AvailableSlotsQueryDto,
|
||||
} from '../dto';
|
||||
|
||||
export class SpecialtyService {
|
||||
private repository: Repository<Specialty>;
|
||||
|
||||
constructor(dataSource: DataSource) {
|
||||
this.repository = dataSource.getRepository(Specialty);
|
||||
}
|
||||
|
||||
async findAll(tenantId: string, activeOnly: boolean = true): Promise<Specialty[]> {
|
||||
const where: any = { tenantId };
|
||||
if (activeOnly) where.active = true;
|
||||
return this.repository.find({ where, order: { name: 'ASC' } });
|
||||
}
|
||||
|
||||
async findById(tenantId: string, id: string): Promise<Specialty | null> {
|
||||
return this.repository.findOne({ where: { id, tenantId } });
|
||||
}
|
||||
|
||||
async create(tenantId: string, dto: CreateSpecialtyDto): Promise<Specialty> {
|
||||
const specialty = this.repository.create({ ...dto, tenantId });
|
||||
return this.repository.save(specialty);
|
||||
}
|
||||
|
||||
async update(tenantId: string, id: string, dto: UpdateSpecialtyDto): Promise<Specialty | null> {
|
||||
const specialty = await this.findById(tenantId, id);
|
||||
if (!specialty) return null;
|
||||
Object.assign(specialty, dto);
|
||||
return this.repository.save(specialty);
|
||||
}
|
||||
}
|
||||
|
||||
export class DoctorService {
|
||||
private repository: Repository<Doctor>;
|
||||
|
||||
constructor(dataSource: DataSource) {
|
||||
this.repository = dataSource.getRepository(Doctor);
|
||||
}
|
||||
|
||||
async findAll(tenantId: string, specialtyId?: string): Promise<Doctor[]> {
|
||||
const where: any = { tenantId, active: true };
|
||||
if (specialtyId) where.specialtyId = specialtyId;
|
||||
|
||||
return this.repository.find({
|
||||
where,
|
||||
relations: ['specialty'],
|
||||
order: { lastName: 'ASC', firstName: 'ASC' },
|
||||
});
|
||||
}
|
||||
|
||||
async findById(tenantId: string, id: string): Promise<Doctor | null> {
|
||||
return this.repository.findOne({
|
||||
where: { id, tenantId },
|
||||
relations: ['specialty'],
|
||||
});
|
||||
}
|
||||
|
||||
async create(tenantId: string, dto: CreateDoctorDto): Promise<Doctor> {
|
||||
const doctor = this.repository.create({ ...dto, tenantId });
|
||||
return this.repository.save(doctor);
|
||||
}
|
||||
|
||||
async update(tenantId: string, id: string, dto: UpdateDoctorDto): Promise<Doctor | null> {
|
||||
const doctor = await this.findById(tenantId, id);
|
||||
if (!doctor) return null;
|
||||
Object.assign(doctor, dto);
|
||||
return this.repository.save(doctor);
|
||||
}
|
||||
|
||||
async softDelete(tenantId: string, id: string): Promise<boolean> {
|
||||
const doctor = await this.findById(tenantId, id);
|
||||
if (!doctor) return false;
|
||||
await this.repository.softDelete(id);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class AppointmentService {
|
||||
private repository: Repository<Appointment>;
|
||||
private slotRepository: Repository<AppointmentSlot>;
|
||||
|
||||
constructor(dataSource: DataSource) {
|
||||
this.repository = dataSource.getRepository(Appointment);
|
||||
this.slotRepository = dataSource.getRepository(AppointmentSlot);
|
||||
}
|
||||
|
||||
async findAll(tenantId: string, query: AppointmentQueryDto): Promise<{ data: Appointment[]; total: number }> {
|
||||
const { patientId, doctorId, dateFrom, dateTo, status, page = 1, limit = 20 } = query;
|
||||
|
||||
const queryBuilder = this.repository.createQueryBuilder('appointment')
|
||||
.leftJoinAndSelect('appointment.doctor', 'doctor')
|
||||
.where('appointment.tenant_id = :tenantId', { tenantId });
|
||||
|
||||
if (patientId) {
|
||||
queryBuilder.andWhere('appointment.patient_id = :patientId', { patientId });
|
||||
}
|
||||
|
||||
if (doctorId) {
|
||||
queryBuilder.andWhere('appointment.doctor_id = :doctorId', { doctorId });
|
||||
}
|
||||
|
||||
if (dateFrom) {
|
||||
queryBuilder.andWhere('appointment.scheduled_date >= :dateFrom', { dateFrom });
|
||||
}
|
||||
|
||||
if (dateTo) {
|
||||
queryBuilder.andWhere('appointment.scheduled_date <= :dateTo', { dateTo });
|
||||
}
|
||||
|
||||
if (status) {
|
||||
queryBuilder.andWhere('appointment.status = :status', { status });
|
||||
}
|
||||
|
||||
queryBuilder
|
||||
.orderBy('appointment.scheduled_date', 'ASC')
|
||||
.addOrderBy('appointment.scheduled_time', 'ASC')
|
||||
.skip((page - 1) * limit)
|
||||
.take(limit);
|
||||
|
||||
const [data, total] = await queryBuilder.getManyAndCount();
|
||||
return { data, total };
|
||||
}
|
||||
|
||||
async findById(tenantId: string, id: string): Promise<Appointment | null> {
|
||||
return this.repository.findOne({
|
||||
where: { id, tenantId },
|
||||
relations: ['doctor'],
|
||||
});
|
||||
}
|
||||
|
||||
async create(tenantId: string, dto: CreateAppointmentDto, createdBy?: string): Promise<Appointment> {
|
||||
const appointment = this.repository.create({
|
||||
...dto,
|
||||
tenantId,
|
||||
scheduledDate: new Date(dto.scheduledDate),
|
||||
createdBy,
|
||||
});
|
||||
return this.repository.save(appointment);
|
||||
}
|
||||
|
||||
async update(tenantId: string, id: string, dto: UpdateAppointmentDto): Promise<Appointment | null> {
|
||||
const appointment = await this.findById(tenantId, id);
|
||||
if (!appointment) return null;
|
||||
|
||||
const updateData: any = { ...dto };
|
||||
if (dto.scheduledDate) {
|
||||
updateData.scheduledDate = new Date(dto.scheduledDate);
|
||||
}
|
||||
|
||||
Object.assign(appointment, updateData);
|
||||
return this.repository.save(appointment);
|
||||
}
|
||||
|
||||
async confirm(tenantId: string, id: string, confirmedBy: string): Promise<Appointment | null> {
|
||||
const appointment = await this.findById(tenantId, id);
|
||||
if (!appointment) return null;
|
||||
|
||||
appointment.status = 'confirmed';
|
||||
appointment.confirmedAt = new Date();
|
||||
appointment.confirmedBy = confirmedBy;
|
||||
return this.repository.save(appointment);
|
||||
}
|
||||
|
||||
async cancel(tenantId: string, id: string, cancelledBy: string, reason?: string): Promise<Appointment | null> {
|
||||
const appointment = await this.findById(tenantId, id);
|
||||
if (!appointment) return null;
|
||||
|
||||
appointment.status = 'cancelled';
|
||||
appointment.cancelledAt = new Date();
|
||||
appointment.cancelledBy = cancelledBy;
|
||||
appointment.cancellationReason = reason;
|
||||
return this.repository.save(appointment);
|
||||
}
|
||||
|
||||
async checkIn(tenantId: string, id: string): Promise<Appointment | null> {
|
||||
const appointment = await this.findById(tenantId, id);
|
||||
if (!appointment) return null;
|
||||
|
||||
appointment.status = 'in_progress';
|
||||
appointment.checkInAt = new Date();
|
||||
return this.repository.save(appointment);
|
||||
}
|
||||
|
||||
async checkOut(tenantId: string, id: string): Promise<Appointment | null> {
|
||||
const appointment = await this.findById(tenantId, id);
|
||||
if (!appointment) return null;
|
||||
|
||||
appointment.status = 'completed';
|
||||
appointment.checkOutAt = new Date();
|
||||
return this.repository.save(appointment);
|
||||
}
|
||||
|
||||
async getAvailableSlots(tenantId: string, doctorId: string, date: string): Promise<string[]> {
|
||||
const targetDate = new Date(date);
|
||||
const dayOfWeek = targetDate.getDay();
|
||||
|
||||
// Obtener slots configurados para ese dia
|
||||
const slots = await this.slotRepository.find({
|
||||
where: {
|
||||
tenantId,
|
||||
doctorId,
|
||||
dayOfWeek,
|
||||
active: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (slots.length === 0) return [];
|
||||
|
||||
// Obtener citas existentes para ese dia
|
||||
const existingAppointments = await this.repository.find({
|
||||
where: {
|
||||
tenantId,
|
||||
doctorId,
|
||||
scheduledDate: targetDate,
|
||||
status: 'cancelled',
|
||||
},
|
||||
});
|
||||
|
||||
const bookedTimes = new Set(
|
||||
existingAppointments
|
||||
.filter(a => a.status !== 'cancelled')
|
||||
.map(a => a.scheduledTime)
|
||||
);
|
||||
|
||||
// Generar slots disponibles
|
||||
const availableSlots: string[] = [];
|
||||
|
||||
for (const slot of slots) {
|
||||
const [startHour, startMin] = slot.startTime.split(':').map(Number);
|
||||
const [endHour, endMin] = slot.endTime.split(':').map(Number);
|
||||
|
||||
let currentHour = startHour;
|
||||
let currentMin = startMin;
|
||||
|
||||
while (currentHour * 60 + currentMin < endHour * 60 + endMin) {
|
||||
const timeStr = `${currentHour.toString().padStart(2, '0')}:${currentMin.toString().padStart(2, '0')}`;
|
||||
|
||||
if (!bookedTimes.has(timeStr)) {
|
||||
availableSlots.push(timeStr);
|
||||
}
|
||||
|
||||
currentMin += slot.slotDurationMinutes;
|
||||
if (currentMin >= 60) {
|
||||
currentHour += Math.floor(currentMin / 60);
|
||||
currentMin = currentMin % 60;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return availableSlots.sort();
|
||||
}
|
||||
}
|
||||
|
||||
export class AppointmentSlotService {
|
||||
private repository: Repository<AppointmentSlot>;
|
||||
|
||||
constructor(dataSource: DataSource) {
|
||||
this.repository = dataSource.getRepository(AppointmentSlot);
|
||||
}
|
||||
|
||||
async findByDoctor(tenantId: string, doctorId: string): Promise<AppointmentSlot[]> {
|
||||
return this.repository.find({
|
||||
where: { tenantId, doctorId, active: true },
|
||||
order: { dayOfWeek: 'ASC', startTime: 'ASC' },
|
||||
});
|
||||
}
|
||||
|
||||
async create(tenantId: string, dto: CreateAppointmentSlotDto): Promise<AppointmentSlot> {
|
||||
const slot = this.repository.create({
|
||||
...dto,
|
||||
tenantId,
|
||||
validFrom: dto.validFrom ? new Date(dto.validFrom) : new Date(),
|
||||
validUntil: dto.validUntil ? new Date(dto.validUntil) : undefined,
|
||||
});
|
||||
return this.repository.save(slot);
|
||||
}
|
||||
|
||||
async delete(tenantId: string, id: string): Promise<boolean> {
|
||||
const slot = await this.repository.findOne({ where: { id, tenantId } });
|
||||
if (!slot) return false;
|
||||
await this.repository.remove(slot);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
19
src/modules/consultations/consultations.module.ts
Normal file
19
src/modules/consultations/consultations.module.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Router } from 'express';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { ConsultationController } from './controllers';
|
||||
|
||||
export interface ConsultationsModuleOptions {
|
||||
dataSource: DataSource;
|
||||
basePath?: string;
|
||||
}
|
||||
|
||||
export class ConsultationsModule {
|
||||
public router: Router;
|
||||
private controller: ConsultationController;
|
||||
|
||||
constructor(options: ConsultationsModuleOptions) {
|
||||
const { dataSource, basePath = '/api' } = options;
|
||||
this.controller = new ConsultationController(dataSource, basePath);
|
||||
this.router = this.controller.router;
|
||||
}
|
||||
}
|
||||
216
src/modules/consultations/controllers/index.ts
Normal file
216
src/modules/consultations/controllers/index.ts
Normal file
@ -0,0 +1,216 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { ConsultationService, DiagnosisService, PrescriptionService, VitalSignsService } from '../services';
|
||||
import {
|
||||
CreateConsultationDto, UpdateConsultationDto, CompleteConsultationDto, ConsultationQueryDto,
|
||||
CreateDiagnosisDto, CreatePrescriptionDto, CreateVitalSignsRecordDto,
|
||||
} from '../dto';
|
||||
|
||||
export class ConsultationController {
|
||||
public router: Router;
|
||||
private consultationService: ConsultationService;
|
||||
private diagnosisService: DiagnosisService;
|
||||
private prescriptionService: PrescriptionService;
|
||||
private vitalSignsService: VitalSignsService;
|
||||
|
||||
constructor(dataSource: DataSource, basePath: string = '/api') {
|
||||
this.router = Router();
|
||||
this.consultationService = new ConsultationService(dataSource);
|
||||
this.diagnosisService = new DiagnosisService(dataSource);
|
||||
this.prescriptionService = new PrescriptionService(dataSource);
|
||||
this.vitalSignsService = new VitalSignsService(dataSource);
|
||||
this.setupRoutes(basePath);
|
||||
}
|
||||
|
||||
private setupRoutes(basePath: string): void {
|
||||
// Consultations
|
||||
this.router.get(`${basePath}/consultations`, this.getConsultations.bind(this));
|
||||
this.router.get(`${basePath}/consultations/:id`, this.getConsultationById.bind(this));
|
||||
this.router.get(`${basePath}/consultations/:id/diagnoses`, this.getConsultationDiagnoses.bind(this));
|
||||
this.router.get(`${basePath}/consultations/:id/prescriptions`, this.getConsultationPrescriptions.bind(this));
|
||||
this.router.post(`${basePath}/consultations`, this.createConsultation.bind(this));
|
||||
this.router.patch(`${basePath}/consultations/:id`, this.updateConsultation.bind(this));
|
||||
this.router.post(`${basePath}/consultations/:id/complete`, this.completeConsultation.bind(this));
|
||||
this.router.post(`${basePath}/consultations/:id/cancel`, this.cancelConsultation.bind(this));
|
||||
|
||||
// Diagnoses
|
||||
this.router.post(`${basePath}/diagnoses`, this.createDiagnosis.bind(this));
|
||||
this.router.delete(`${basePath}/diagnoses/:id`, this.deleteDiagnosis.bind(this));
|
||||
|
||||
// Prescriptions
|
||||
this.router.get(`${basePath}/prescriptions/:id`, this.getPrescriptionById.bind(this));
|
||||
this.router.get(`${basePath}/patients/:patientId/prescriptions`, this.getPatientPrescriptions.bind(this));
|
||||
this.router.post(`${basePath}/prescriptions`, this.createPrescription.bind(this));
|
||||
|
||||
// Vital Signs
|
||||
this.router.get(`${basePath}/patients/:patientId/vital-signs`, this.getPatientVitalSigns.bind(this));
|
||||
this.router.post(`${basePath}/vital-signs`, this.createVitalSigns.bind(this));
|
||||
}
|
||||
|
||||
private getTenantId(req: Request): string {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) throw new Error('x-tenant-id header is required');
|
||||
return tenantId;
|
||||
}
|
||||
|
||||
private getUserId(req: Request): string | undefined {
|
||||
return req.headers['x-user-id'] as string;
|
||||
}
|
||||
|
||||
// Consultations
|
||||
private async getConsultations(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const query: ConsultationQueryDto = {
|
||||
patientId: req.query.patientId as string,
|
||||
doctorId: req.query.doctorId as string,
|
||||
dateFrom: req.query.dateFrom as string,
|
||||
dateTo: req.query.dateTo as string,
|
||||
status: req.query.status as any,
|
||||
page: req.query.page ? parseInt(req.query.page as string) : 1,
|
||||
limit: req.query.limit ? parseInt(req.query.limit as string) : 20,
|
||||
};
|
||||
const result = await this.consultationService.findAll(tenantId, query);
|
||||
res.json({ data: result.data, meta: { total: result.total, page: query.page, limit: query.limit } });
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
|
||||
private async getConsultationById(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const { id } = req.params;
|
||||
const data = await this.consultationService.findById(tenantId, id);
|
||||
if (!data) { res.status(404).json({ error: 'Consultation not found' }); return; }
|
||||
res.json({ data });
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
|
||||
private async getConsultationDiagnoses(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const { id } = req.params;
|
||||
const data = await this.diagnosisService.findByConsultation(tenantId, id);
|
||||
res.json({ data });
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
|
||||
private async getConsultationPrescriptions(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const { id } = req.params;
|
||||
const data = await this.prescriptionService.findByConsultation(tenantId, id);
|
||||
res.json({ data });
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
|
||||
private async createConsultation(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const dto: CreateConsultationDto = req.body;
|
||||
const data = await this.consultationService.create(tenantId, dto);
|
||||
res.status(201).json({ data });
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
|
||||
private async updateConsultation(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const { id } = req.params;
|
||||
const dto: UpdateConsultationDto = req.body;
|
||||
const data = await this.consultationService.update(tenantId, id, dto);
|
||||
if (!data) { res.status(404).json({ error: 'Consultation not found' }); return; }
|
||||
res.json({ data });
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
|
||||
private async completeConsultation(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const { id } = req.params;
|
||||
const dto: CompleteConsultationDto = req.body;
|
||||
const data = await this.consultationService.complete(tenantId, id, dto);
|
||||
if (!data) { res.status(404).json({ error: 'Consultation not found' }); return; }
|
||||
res.json({ data });
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
|
||||
private async cancelConsultation(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const { id } = req.params;
|
||||
const data = await this.consultationService.cancel(tenantId, id);
|
||||
if (!data) { res.status(404).json({ error: 'Consultation not found' }); return; }
|
||||
res.json({ data });
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
|
||||
// Diagnoses
|
||||
private async createDiagnosis(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const dto: CreateDiagnosisDto = req.body;
|
||||
const data = await this.diagnosisService.create(tenantId, dto);
|
||||
res.status(201).json({ data });
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
|
||||
private async deleteDiagnosis(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const { id } = req.params;
|
||||
const deleted = await this.diagnosisService.delete(tenantId, id);
|
||||
if (!deleted) { res.status(404).json({ error: 'Diagnosis not found' }); return; }
|
||||
res.status(204).send();
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
|
||||
// Prescriptions
|
||||
private async getPrescriptionById(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const { id } = req.params;
|
||||
const data = await this.prescriptionService.findById(tenantId, id);
|
||||
if (!data) { res.status(404).json({ error: 'Prescription not found' }); return; }
|
||||
res.json({ data });
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
|
||||
private async getPatientPrescriptions(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const { patientId } = req.params;
|
||||
const data = await this.prescriptionService.findByPatient(tenantId, patientId);
|
||||
res.json({ data });
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
|
||||
private async createPrescription(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const dto: CreatePrescriptionDto = req.body;
|
||||
const data = await this.prescriptionService.create(tenantId, dto);
|
||||
res.status(201).json({ data });
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
|
||||
// Vital Signs
|
||||
private async getPatientVitalSigns(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const { patientId } = req.params;
|
||||
const limit = req.query.limit ? parseInt(req.query.limit as string) : 10;
|
||||
const data = await this.vitalSignsService.findByPatient(tenantId, patientId, limit);
|
||||
res.json({ data });
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
|
||||
private async createVitalSigns(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const userId = this.getUserId(req);
|
||||
const dto: CreateVitalSignsRecordDto = req.body;
|
||||
const data = await this.vitalSignsService.create(tenantId, dto, userId);
|
||||
res.status(201).json({ data });
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
}
|
||||
283
src/modules/consultations/dto/index.ts
Normal file
283
src/modules/consultations/dto/index.ts
Normal file
@ -0,0 +1,283 @@
|
||||
import { IsString, IsOptional, IsUUID, IsEnum, IsBoolean, IsDateString, IsInt, IsNumber, IsArray, ValidateNested, MaxLength, Min, Max } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ConsultationStatus, DiagnosisType, PrescriptionStatus, VitalSigns } from '../entities';
|
||||
|
||||
// Vital Signs DTO
|
||||
export class VitalSignsDto implements VitalSigns {
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Max(500)
|
||||
weight_kg?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Max(300)
|
||||
height_cm?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(30)
|
||||
@Max(45)
|
||||
temperature?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
blood_pressure?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@Max(300)
|
||||
heart_rate?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@Max(100)
|
||||
respiratory_rate?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@Max(100)
|
||||
oxygen_saturation?: number;
|
||||
}
|
||||
|
||||
// Consultation DTOs
|
||||
export class CreateConsultationDto {
|
||||
@IsUUID()
|
||||
patientId: string;
|
||||
|
||||
@IsUUID()
|
||||
doctorId: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
appointmentId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
chiefComplaint?: string;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => VitalSignsDto)
|
||||
vitalSigns?: VitalSignsDto;
|
||||
}
|
||||
|
||||
export class UpdateConsultationDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
chiefComplaint?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
presentIllness?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
physicalExamination?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
assessment?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
plan?: string;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => VitalSignsDto)
|
||||
vitalSigns?: VitalSignsDto;
|
||||
}
|
||||
|
||||
export class CompleteConsultationDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
physicalExamination?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
assessment?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
plan?: string;
|
||||
}
|
||||
|
||||
export class ConsultationQueryDto {
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
patientId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
doctorId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
dateFrom?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
dateTo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(['in_progress', 'completed', 'cancelled'])
|
||||
status?: ConsultationStatus;
|
||||
|
||||
@IsOptional()
|
||||
page?: number;
|
||||
|
||||
@IsOptional()
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
// Diagnosis DTOs
|
||||
export class CreateDiagnosisDto {
|
||||
@IsUUID()
|
||||
consultationId: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(10)
|
||||
icd10Code?: string;
|
||||
|
||||
@IsString()
|
||||
description: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isPrimary?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(['presumptive', 'definitive', 'differential'])
|
||||
diagnosisType?: DiagnosisType;
|
||||
}
|
||||
|
||||
// Prescription DTOs
|
||||
export class PrescriptionItemDto {
|
||||
@IsString()
|
||||
@MaxLength(200)
|
||||
medicationName: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(50)
|
||||
medicationCode?: string;
|
||||
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
dosage: string;
|
||||
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
frequency: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
duration?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
quantity?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
instructions?: string;
|
||||
}
|
||||
|
||||
export class CreatePrescriptionDto {
|
||||
@IsUUID()
|
||||
consultationId: string;
|
||||
|
||||
@IsUUID()
|
||||
patientId: string;
|
||||
|
||||
@IsUUID()
|
||||
doctorId: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
instructions?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
notes?: string;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => PrescriptionItemDto)
|
||||
items: PrescriptionItemDto[];
|
||||
}
|
||||
|
||||
// Vital Signs Record DTOs
|
||||
export class CreateVitalSignsRecordDto {
|
||||
@IsUUID()
|
||||
patientId: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
consultationId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Max(500)
|
||||
weightKg?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Max(300)
|
||||
heightCm?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(30)
|
||||
@Max(45)
|
||||
temperatureCelsius?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(50)
|
||||
@Max(250)
|
||||
bloodPressureSystolic?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(30)
|
||||
@Max(150)
|
||||
bloodPressureDiastolic?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(30)
|
||||
@Max(250)
|
||||
heartRate?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(5)
|
||||
@Max(60)
|
||||
respiratoryRate?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(50)
|
||||
@Max(100)
|
||||
oxygenSaturation?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
glucoseMgDl?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
notes?: string;
|
||||
}
|
||||
80
src/modules/consultations/entities/consultation.entity.ts
Normal file
80
src/modules/consultations/entities/consultation.entity.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
Index,
|
||||
OneToMany,
|
||||
} from 'typeorm';
|
||||
|
||||
export type ConsultationStatus = 'in_progress' | 'completed' | 'cancelled';
|
||||
|
||||
export interface VitalSigns {
|
||||
weight_kg?: number;
|
||||
height_cm?: number;
|
||||
temperature?: number;
|
||||
blood_pressure?: string; // "120/80"
|
||||
heart_rate?: number;
|
||||
respiratory_rate?: number;
|
||||
oxygen_saturation?: number;
|
||||
}
|
||||
|
||||
@Entity({ name: 'consultations', schema: 'clinica' })
|
||||
export class Consultation {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||
tenantId: string;
|
||||
|
||||
// Relaciones
|
||||
@Index()
|
||||
@Column({ name: 'patient_id', type: 'uuid' })
|
||||
patientId: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'doctor_id', type: 'uuid' })
|
||||
doctorId: string;
|
||||
|
||||
@Column({ name: 'appointment_id', type: 'uuid', nullable: true })
|
||||
appointmentId?: string;
|
||||
|
||||
// Tiempo
|
||||
@Column({ name: 'started_at', type: 'timestamptz', default: () => 'NOW()' })
|
||||
startedAt: Date;
|
||||
|
||||
@Column({ name: 'ended_at', type: 'timestamptz', nullable: true })
|
||||
endedAt?: Date;
|
||||
|
||||
// Contenido clinico
|
||||
@Column({ name: 'chief_complaint', type: 'text', nullable: true })
|
||||
chiefComplaint?: string;
|
||||
|
||||
@Column({ name: 'present_illness', type: 'text', nullable: true })
|
||||
presentIllness?: string;
|
||||
|
||||
@Column({ name: 'physical_examination', type: 'text', nullable: true })
|
||||
physicalExamination?: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
assessment?: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
plan?: string;
|
||||
|
||||
// Signos vitales (snapshot)
|
||||
@Column({ name: 'vital_signs', type: 'jsonb', nullable: true })
|
||||
vitalSigns?: VitalSigns;
|
||||
|
||||
// Estado
|
||||
@Column({ type: 'enum', enum: ['in_progress', 'completed', 'cancelled'], default: 'in_progress' })
|
||||
status: ConsultationStatus;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
47
src/modules/consultations/entities/diagnosis.entity.ts
Normal file
47
src/modules/consultations/entities/diagnosis.entity.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Index,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { Consultation } from './consultation.entity';
|
||||
|
||||
export type DiagnosisType = 'presumptive' | 'definitive' | 'differential';
|
||||
|
||||
@Entity({ name: 'diagnoses', schema: 'clinica' })
|
||||
export class Diagnosis {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||
tenantId: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'consultation_id', type: 'uuid' })
|
||||
consultationId: string;
|
||||
|
||||
@ManyToOne(() => Consultation, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'consultation_id' })
|
||||
consultation: Consultation;
|
||||
|
||||
// Diagnostico CIE-10
|
||||
@Index()
|
||||
@Column({ name: 'icd10_code', type: 'varchar', length: 10, nullable: true })
|
||||
icd10Code?: string;
|
||||
|
||||
@Column({ type: 'text' })
|
||||
description: string;
|
||||
|
||||
@Column({ name: 'is_primary', type: 'boolean', default: false })
|
||||
isPrimary: boolean;
|
||||
|
||||
@Column({ name: 'diagnosis_type', type: 'varchar', length: 20, default: 'definitive' })
|
||||
diagnosisType: DiagnosisType;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
}
|
||||
5
src/modules/consultations/entities/index.ts
Normal file
5
src/modules/consultations/entities/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export { Consultation, ConsultationStatus, VitalSigns } from './consultation.entity';
|
||||
export { Diagnosis, DiagnosisType } from './diagnosis.entity';
|
||||
export { Prescription, PrescriptionStatus } from './prescription.entity';
|
||||
export { PrescriptionItem } from './prescription-item.entity';
|
||||
export { VitalSignsRecord } from './vital-signs.entity';
|
||||
@ -0,0 +1,58 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Index,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { Prescription } from './prescription.entity';
|
||||
|
||||
@Entity({ name: 'prescription_items', schema: 'clinica' })
|
||||
export class PrescriptionItem {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||
tenantId: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'prescription_id', type: 'uuid' })
|
||||
prescriptionId: string;
|
||||
|
||||
@ManyToOne(() => Prescription, prescription => prescription.items, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'prescription_id' })
|
||||
prescription: Prescription;
|
||||
|
||||
// Medicamento
|
||||
@Column({ name: 'medication_name', type: 'varchar', length: 200 })
|
||||
medicationName: string;
|
||||
|
||||
@Column({ name: 'medication_code', type: 'varchar', length: 50, nullable: true })
|
||||
medicationCode?: string;
|
||||
|
||||
// Dosificacion
|
||||
@Column({ type: 'varchar', length: 100 })
|
||||
dosage: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 100 })
|
||||
frequency: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 100, nullable: true })
|
||||
duration?: string;
|
||||
|
||||
@Column({ type: 'integer', nullable: true })
|
||||
quantity?: number;
|
||||
|
||||
// Instrucciones
|
||||
@Column({ type: 'text', nullable: true })
|
||||
instructions?: string;
|
||||
|
||||
@Column({ type: 'integer', default: 1 })
|
||||
sequence: number;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
}
|
||||
60
src/modules/consultations/entities/prescription.entity.ts
Normal file
60
src/modules/consultations/entities/prescription.entity.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
Index,
|
||||
OneToMany,
|
||||
} from 'typeorm';
|
||||
import { PrescriptionItem } from './prescription-item.entity';
|
||||
|
||||
export type PrescriptionStatus = 'active' | 'completed' | 'cancelled';
|
||||
|
||||
@Entity({ name: 'prescriptions', schema: 'clinica' })
|
||||
export class Prescription {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||
tenantId: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'consultation_id', type: 'uuid' })
|
||||
consultationId: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'patient_id', type: 'uuid' })
|
||||
patientId: string;
|
||||
|
||||
@Column({ name: 'doctor_id', type: 'uuid' })
|
||||
doctorId: string;
|
||||
|
||||
// Datos de la receta
|
||||
@Column({ name: 'prescription_number', type: 'varchar', length: 50, nullable: true })
|
||||
prescriptionNumber?: string;
|
||||
|
||||
@Column({ name: 'prescription_date', type: 'date', default: () => 'CURRENT_DATE' })
|
||||
prescriptionDate: Date;
|
||||
|
||||
// Estado
|
||||
@Column({ type: 'enum', enum: ['active', 'completed', 'cancelled'], default: 'active' })
|
||||
status: PrescriptionStatus;
|
||||
|
||||
// Notas
|
||||
@Column({ type: 'text', nullable: true })
|
||||
instructions?: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
notes?: string;
|
||||
|
||||
@OneToMany(() => PrescriptionItem, item => item.prescription)
|
||||
items: PrescriptionItem[];
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
61
src/modules/consultations/entities/vital-signs.entity.ts
Normal file
61
src/modules/consultations/entities/vital-signs.entity.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
Index,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity({ name: 'vital_signs', schema: 'clinica' })
|
||||
export class VitalSignsRecord {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||
tenantId: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'patient_id', type: 'uuid' })
|
||||
patientId: string;
|
||||
|
||||
@Column({ name: 'consultation_id', type: 'uuid', nullable: true })
|
||||
consultationId?: string;
|
||||
|
||||
@Column({ name: 'recorded_by', type: 'uuid', nullable: true })
|
||||
recordedBy?: string;
|
||||
|
||||
// Mediciones
|
||||
@Column({ name: 'weight_kg', type: 'numeric', precision: 5, scale: 2, nullable: true })
|
||||
weightKg?: number;
|
||||
|
||||
@Column({ name: 'height_cm', type: 'numeric', precision: 5, scale: 2, nullable: true })
|
||||
heightCm?: number;
|
||||
|
||||
@Column({ name: 'temperature_celsius', type: 'numeric', precision: 4, scale: 1, nullable: true })
|
||||
temperatureCelsius?: number;
|
||||
|
||||
@Column({ name: 'blood_pressure_systolic', type: 'integer', nullable: true })
|
||||
bloodPressureSystolic?: number;
|
||||
|
||||
@Column({ name: 'blood_pressure_diastolic', type: 'integer', nullable: true })
|
||||
bloodPressureDiastolic?: number;
|
||||
|
||||
@Column({ name: 'heart_rate', type: 'integer', nullable: true })
|
||||
heartRate?: number;
|
||||
|
||||
@Column({ name: 'respiratory_rate', type: 'integer', nullable: true })
|
||||
respiratoryRate?: number;
|
||||
|
||||
@Column({ name: 'oxygen_saturation', type: 'integer', nullable: true })
|
||||
oxygenSaturation?: number;
|
||||
|
||||
@Column({ name: 'glucose_mg_dl', type: 'numeric', precision: 5, scale: 1, nullable: true })
|
||||
glucoseMgDl?: number;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
notes?: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'recorded_at', type: 'timestamptz', default: () => 'NOW()' })
|
||||
recordedAt: Date;
|
||||
}
|
||||
11
src/modules/consultations/index.ts
Normal file
11
src/modules/consultations/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export { ConsultationsModule, ConsultationsModuleOptions } from './consultations.module';
|
||||
export {
|
||||
Consultation, ConsultationStatus, VitalSigns,
|
||||
Diagnosis, DiagnosisType,
|
||||
Prescription, PrescriptionStatus,
|
||||
PrescriptionItem,
|
||||
VitalSignsRecord,
|
||||
} from './entities';
|
||||
export { ConsultationService, DiagnosisService, PrescriptionService, VitalSignsService } from './services';
|
||||
export { ConsultationController } from './controllers';
|
||||
export * from './dto';
|
||||
204
src/modules/consultations/services/index.ts
Normal file
204
src/modules/consultations/services/index.ts
Normal file
@ -0,0 +1,204 @@
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
import { Consultation, Diagnosis, Prescription, PrescriptionItem, VitalSignsRecord } from '../entities';
|
||||
import {
|
||||
CreateConsultationDto, UpdateConsultationDto, CompleteConsultationDto, ConsultationQueryDto,
|
||||
CreateDiagnosisDto, CreatePrescriptionDto, CreateVitalSignsRecordDto,
|
||||
} from '../dto';
|
||||
|
||||
export class ConsultationService {
|
||||
private repository: Repository<Consultation>;
|
||||
|
||||
constructor(dataSource: DataSource) {
|
||||
this.repository = dataSource.getRepository(Consultation);
|
||||
}
|
||||
|
||||
async findAll(tenantId: string, query: ConsultationQueryDto): Promise<{ data: Consultation[]; total: number }> {
|
||||
const { patientId, doctorId, dateFrom, dateTo, status, page = 1, limit = 20 } = query;
|
||||
|
||||
const queryBuilder = this.repository.createQueryBuilder('consultation')
|
||||
.where('consultation.tenant_id = :tenantId', { tenantId });
|
||||
|
||||
if (patientId) queryBuilder.andWhere('consultation.patient_id = :patientId', { patientId });
|
||||
if (doctorId) queryBuilder.andWhere('consultation.doctor_id = :doctorId', { doctorId });
|
||||
if (dateFrom) queryBuilder.andWhere('consultation.started_at >= :dateFrom', { dateFrom });
|
||||
if (dateTo) queryBuilder.andWhere('consultation.started_at <= :dateTo', { dateTo });
|
||||
if (status) queryBuilder.andWhere('consultation.status = :status', { status });
|
||||
|
||||
queryBuilder
|
||||
.orderBy('consultation.started_at', 'DESC')
|
||||
.skip((page - 1) * limit)
|
||||
.take(limit);
|
||||
|
||||
const [data, total] = await queryBuilder.getManyAndCount();
|
||||
return { data, total };
|
||||
}
|
||||
|
||||
async findById(tenantId: string, id: string): Promise<Consultation | null> {
|
||||
return this.repository.findOne({ where: { id, tenantId } });
|
||||
}
|
||||
|
||||
async findByPatient(tenantId: string, patientId: string): Promise<Consultation[]> {
|
||||
return this.repository.find({
|
||||
where: { tenantId, patientId },
|
||||
order: { startedAt: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
async create(tenantId: string, dto: CreateConsultationDto): Promise<Consultation> {
|
||||
const consultation = this.repository.create({ ...dto, tenantId });
|
||||
return this.repository.save(consultation);
|
||||
}
|
||||
|
||||
async update(tenantId: string, id: string, dto: UpdateConsultationDto): Promise<Consultation | null> {
|
||||
const consultation = await this.findById(tenantId, id);
|
||||
if (!consultation) return null;
|
||||
Object.assign(consultation, dto);
|
||||
return this.repository.save(consultation);
|
||||
}
|
||||
|
||||
async complete(tenantId: string, id: string, dto: CompleteConsultationDto): Promise<Consultation | null> {
|
||||
const consultation = await this.findById(tenantId, id);
|
||||
if (!consultation) return null;
|
||||
|
||||
consultation.status = 'completed';
|
||||
consultation.endedAt = new Date();
|
||||
if (dto.physicalExamination) consultation.physicalExamination = dto.physicalExamination;
|
||||
if (dto.assessment) consultation.assessment = dto.assessment;
|
||||
if (dto.plan) consultation.plan = dto.plan;
|
||||
|
||||
return this.repository.save(consultation);
|
||||
}
|
||||
|
||||
async cancel(tenantId: string, id: string): Promise<Consultation | null> {
|
||||
const consultation = await this.findById(tenantId, id);
|
||||
if (!consultation) return null;
|
||||
|
||||
consultation.status = 'cancelled';
|
||||
consultation.endedAt = new Date();
|
||||
return this.repository.save(consultation);
|
||||
}
|
||||
}
|
||||
|
||||
export class DiagnosisService {
|
||||
private repository: Repository<Diagnosis>;
|
||||
|
||||
constructor(dataSource: DataSource) {
|
||||
this.repository = dataSource.getRepository(Diagnosis);
|
||||
}
|
||||
|
||||
async findByConsultation(tenantId: string, consultationId: string): Promise<Diagnosis[]> {
|
||||
return this.repository.find({
|
||||
where: { tenantId, consultationId },
|
||||
order: { isPrimary: 'DESC', createdAt: 'ASC' },
|
||||
});
|
||||
}
|
||||
|
||||
async create(tenantId: string, dto: CreateDiagnosisDto): Promise<Diagnosis> {
|
||||
const diagnosis = this.repository.create({ ...dto, tenantId });
|
||||
return this.repository.save(diagnosis);
|
||||
}
|
||||
|
||||
async delete(tenantId: string, id: string): Promise<boolean> {
|
||||
const diagnosis = await this.repository.findOne({ where: { id, tenantId } });
|
||||
if (!diagnosis) return false;
|
||||
await this.repository.remove(diagnosis);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class PrescriptionService {
|
||||
private repository: Repository<Prescription>;
|
||||
private itemRepository: Repository<PrescriptionItem>;
|
||||
|
||||
constructor(dataSource: DataSource) {
|
||||
this.repository = dataSource.getRepository(Prescription);
|
||||
this.itemRepository = dataSource.getRepository(PrescriptionItem);
|
||||
}
|
||||
|
||||
async findByConsultation(tenantId: string, consultationId: string): Promise<Prescription[]> {
|
||||
return this.repository.find({
|
||||
where: { tenantId, consultationId },
|
||||
relations: ['items'],
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
async findByPatient(tenantId: string, patientId: string): Promise<Prescription[]> {
|
||||
return this.repository.find({
|
||||
where: { tenantId, patientId },
|
||||
relations: ['items'],
|
||||
order: { prescriptionDate: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
async findById(tenantId: string, id: string): Promise<Prescription | null> {
|
||||
return this.repository.findOne({
|
||||
where: { id, tenantId },
|
||||
relations: ['items'],
|
||||
});
|
||||
}
|
||||
|
||||
async create(tenantId: string, dto: CreatePrescriptionDto): Promise<Prescription> {
|
||||
// Generar numero de receta
|
||||
const count = await this.repository.count({ where: { tenantId } });
|
||||
const prescriptionNumber = `RX-${Date.now().toString(36).toUpperCase()}-${(count + 1).toString().padStart(4, '0')}`;
|
||||
|
||||
const prescription = this.repository.create({
|
||||
tenantId,
|
||||
consultationId: dto.consultationId,
|
||||
patientId: dto.patientId,
|
||||
doctorId: dto.doctorId,
|
||||
prescriptionNumber,
|
||||
instructions: dto.instructions,
|
||||
notes: dto.notes,
|
||||
});
|
||||
|
||||
const savedPrescription = await this.repository.save(prescription);
|
||||
|
||||
// Crear items
|
||||
const items = dto.items.map((item, index) =>
|
||||
this.itemRepository.create({
|
||||
...item,
|
||||
tenantId,
|
||||
prescriptionId: savedPrescription.id,
|
||||
sequence: index + 1,
|
||||
})
|
||||
);
|
||||
|
||||
await this.itemRepository.save(items);
|
||||
|
||||
return this.findById(tenantId, savedPrescription.id) as Promise<Prescription>;
|
||||
}
|
||||
}
|
||||
|
||||
export class VitalSignsService {
|
||||
private repository: Repository<VitalSignsRecord>;
|
||||
|
||||
constructor(dataSource: DataSource) {
|
||||
this.repository = dataSource.getRepository(VitalSignsRecord);
|
||||
}
|
||||
|
||||
async findByPatient(tenantId: string, patientId: string, limit: number = 10): Promise<VitalSignsRecord[]> {
|
||||
return this.repository.find({
|
||||
where: { tenantId, patientId },
|
||||
order: { recordedAt: 'DESC' },
|
||||
take: limit,
|
||||
});
|
||||
}
|
||||
|
||||
async create(tenantId: string, dto: CreateVitalSignsRecordDto, recordedBy?: string): Promise<VitalSignsRecord> {
|
||||
const record = this.repository.create({
|
||||
...dto,
|
||||
tenantId,
|
||||
recordedBy,
|
||||
});
|
||||
return this.repository.save(record);
|
||||
}
|
||||
|
||||
async getLatest(tenantId: string, patientId: string): Promise<VitalSignsRecord | null> {
|
||||
return this.repository.findOne({
|
||||
where: { tenantId, patientId },
|
||||
order: { recordedAt: 'DESC' },
|
||||
});
|
||||
}
|
||||
}
|
||||
152
src/modules/patients/controllers/index.ts
Normal file
152
src/modules/patients/controllers/index.ts
Normal file
@ -0,0 +1,152 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { PatientService } from '../services';
|
||||
import { CreatePatientDto, UpdatePatientDto, PatientQueryDto } from '../dto';
|
||||
|
||||
export class PatientController {
|
||||
public router: Router;
|
||||
private service: PatientService;
|
||||
|
||||
constructor(dataSource: DataSource, basePath: string = '/api') {
|
||||
this.router = Router();
|
||||
this.service = new PatientService(dataSource);
|
||||
this.setupRoutes(basePath);
|
||||
}
|
||||
|
||||
private setupRoutes(basePath: string): void {
|
||||
const path = `${basePath}/patients`;
|
||||
|
||||
// GET /patients - Listar pacientes
|
||||
this.router.get(path, this.findAll.bind(this));
|
||||
|
||||
// GET /patients/:id - Obtener paciente por ID
|
||||
this.router.get(`${path}/:id`, this.findById.bind(this));
|
||||
|
||||
// GET /patients/:id/history - Obtener historial del paciente
|
||||
this.router.get(`${path}/:id/history`, this.getHistory.bind(this));
|
||||
|
||||
// POST /patients - Crear paciente
|
||||
this.router.post(path, this.create.bind(this));
|
||||
|
||||
// PATCH /patients/:id - Actualizar paciente
|
||||
this.router.patch(`${path}/:id`, this.update.bind(this));
|
||||
|
||||
// DELETE /patients/:id - Eliminar paciente (soft delete)
|
||||
this.router.delete(`${path}/:id`, this.delete.bind(this));
|
||||
}
|
||||
|
||||
private getTenantId(req: Request): string {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
throw new Error('x-tenant-id header is required');
|
||||
}
|
||||
return tenantId;
|
||||
}
|
||||
|
||||
private async findAll(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const query: PatientQueryDto = {
|
||||
search: req.query.search as string,
|
||||
status: req.query.status as any,
|
||||
hasInsurance: req.query.hasInsurance === 'true' ? true : req.query.hasInsurance === 'false' ? false : undefined,
|
||||
page: req.query.page ? parseInt(req.query.page as string) : 1,
|
||||
limit: req.query.limit ? parseInt(req.query.limit as string) : 20,
|
||||
};
|
||||
|
||||
const result = await this.service.findAll(tenantId, query);
|
||||
res.json({
|
||||
data: result.data,
|
||||
meta: {
|
||||
total: result.total,
|
||||
page: query.page,
|
||||
limit: query.limit,
|
||||
totalPages: Math.ceil(result.total / (query.limit || 20)),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
private async findById(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const { id } = req.params;
|
||||
|
||||
const patient = await this.service.findById(tenantId, id);
|
||||
if (!patient) {
|
||||
res.status(404).json({ error: 'Patient not found', code: 'PATIENT_NOT_FOUND' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ data: patient });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
private async getHistory(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const { id } = req.params;
|
||||
|
||||
const history = await this.service.getPatientHistory(tenantId, id);
|
||||
if (!history) {
|
||||
res.status(404).json({ error: 'Patient not found', code: 'PATIENT_NOT_FOUND' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ data: history });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
private async create(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const dto: CreatePatientDto = req.body;
|
||||
|
||||
const patient = await this.service.create(tenantId, dto);
|
||||
res.status(201).json({ data: patient });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
private async update(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const { id } = req.params;
|
||||
const dto: UpdatePatientDto = req.body;
|
||||
|
||||
const patient = await this.service.update(tenantId, id, dto);
|
||||
if (!patient) {
|
||||
res.status(404).json({ error: 'Patient not found', code: 'PATIENT_NOT_FOUND' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ data: patient });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
private async delete(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
const { id } = req.params;
|
||||
|
||||
const deleted = await this.service.softDelete(tenantId, id);
|
||||
if (!deleted) {
|
||||
res.status(404).json({ error: 'Patient not found', code: 'PATIENT_NOT_FOUND' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
228
src/modules/patients/dto/index.ts
Normal file
228
src/modules/patients/dto/index.ts
Normal file
@ -0,0 +1,228 @@
|
||||
import { IsString, IsOptional, IsEmail, IsEnum, IsBoolean, IsArray, IsDateString, IsUUID, ValidateNested, MaxLength } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { PatientStatus, Gender, BloodType, PatientAddress } from '../entities';
|
||||
|
||||
export class AddressDto implements PatientAddress {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(255)
|
||||
street?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
city?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
state?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(20)
|
||||
zip?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
country?: string;
|
||||
}
|
||||
|
||||
export class CreatePatientDto {
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
firstName: string;
|
||||
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
lastName: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
dateOfBirth?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(['male', 'female', 'other', 'unknown'])
|
||||
gender?: Gender;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(18)
|
||||
curp?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(13)
|
||||
rfc?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
@MaxLength(255)
|
||||
email?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(20)
|
||||
phone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(20)
|
||||
mobile?: string;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => AddressDto)
|
||||
address?: AddressDto;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(['A+', 'A-', 'B+', 'B-', 'AB+', 'AB-', 'O+', 'O-', 'unknown'])
|
||||
bloodType?: BloodType;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
allergies?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
chronicConditions?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(200)
|
||||
emergencyContactName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(20)
|
||||
emergencyContactPhone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
hasInsurance?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
insuranceProvider?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(50)
|
||||
insurancePolicyNumber?: string;
|
||||
}
|
||||
|
||||
export class UpdatePatientDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
firstName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
lastName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
dateOfBirth?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(['male', 'female', 'other', 'unknown'])
|
||||
gender?: Gender;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(18)
|
||||
curp?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(13)
|
||||
rfc?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
@MaxLength(255)
|
||||
email?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(20)
|
||||
phone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(20)
|
||||
mobile?: string;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => AddressDto)
|
||||
address?: AddressDto;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(['A+', 'A-', 'B+', 'B-', 'AB+', 'AB-', 'O+', 'O-', 'unknown'])
|
||||
bloodType?: BloodType;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
allergies?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
chronicConditions?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(200)
|
||||
emergencyContactName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(20)
|
||||
emergencyContactPhone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
hasInsurance?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
insuranceProvider?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(50)
|
||||
insurancePolicyNumber?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(['active', 'inactive', 'deceased'])
|
||||
status?: PatientStatus;
|
||||
}
|
||||
|
||||
export class PatientQueryDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
search?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(['active', 'inactive', 'deceased'])
|
||||
status?: PatientStatus;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
hasInsurance?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
page?: number;
|
||||
|
||||
@IsOptional()
|
||||
limit?: number;
|
||||
}
|
||||
1
src/modules/patients/entities/index.ts
Normal file
1
src/modules/patients/entities/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { Patient, PatientStatus, Gender, BloodType, PatientAddress } from './patient.entity';
|
||||
112
src/modules/patients/entities/patient.entity.ts
Normal file
112
src/modules/patients/entities/patient.entity.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Index,
|
||||
} from 'typeorm';
|
||||
|
||||
export type PatientStatus = 'active' | 'inactive' | 'deceased';
|
||||
export type Gender = 'male' | 'female' | 'other' | 'unknown';
|
||||
export type BloodType = 'A+' | 'A-' | 'B+' | 'B-' | 'AB+' | 'AB-' | 'O+' | 'O-' | 'unknown';
|
||||
|
||||
export interface PatientAddress {
|
||||
street?: string;
|
||||
city?: string;
|
||||
state?: string;
|
||||
zip?: string;
|
||||
country?: string;
|
||||
}
|
||||
|
||||
@Entity({ name: 'patients', schema: 'clinica' })
|
||||
export class Patient {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||
tenantId: string;
|
||||
|
||||
@Column({ name: 'partner_id', type: 'uuid', nullable: true })
|
||||
partnerId?: string;
|
||||
|
||||
// Datos personales
|
||||
@Column({ name: 'first_name', type: 'varchar', length: 100 })
|
||||
firstName: string;
|
||||
|
||||
@Column({ name: 'last_name', type: 'varchar', length: 100 })
|
||||
lastName: string;
|
||||
|
||||
@Column({ name: 'date_of_birth', type: 'date', nullable: true })
|
||||
dateOfBirth?: Date;
|
||||
|
||||
@Column({ type: 'enum', enum: ['male', 'female', 'other', 'unknown'], default: 'unknown' })
|
||||
gender: Gender;
|
||||
|
||||
// Identificacion
|
||||
@Column({ type: 'varchar', length: 18, nullable: true })
|
||||
curp?: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 13, nullable: true })
|
||||
rfc?: string;
|
||||
|
||||
// Contacto
|
||||
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||
email?: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 20, nullable: true })
|
||||
phone?: string;
|
||||
|
||||
@Index()
|
||||
@Column({ type: 'varchar', length: 20, nullable: true })
|
||||
mobile?: string;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
address?: PatientAddress;
|
||||
|
||||
// Datos medicos basicos
|
||||
@Column({ name: 'blood_type', type: 'enum', enum: ['A+', 'A-', 'B+', 'B-', 'AB+', 'AB-', 'O+', 'O-', 'unknown'], default: 'unknown' })
|
||||
bloodType: BloodType;
|
||||
|
||||
@Column({ type: 'text', array: true, nullable: true })
|
||||
allergies?: string[];
|
||||
|
||||
@Column({ name: 'chronic_conditions', type: 'text', array: true, nullable: true })
|
||||
chronicConditions?: string[];
|
||||
|
||||
@Column({ name: 'emergency_contact_name', type: 'varchar', length: 200, nullable: true })
|
||||
emergencyContactName?: string;
|
||||
|
||||
@Column({ name: 'emergency_contact_phone', type: 'varchar', length: 20, nullable: true })
|
||||
emergencyContactPhone?: string;
|
||||
|
||||
// Seguro medico
|
||||
@Column({ name: 'has_insurance', type: 'boolean', default: false })
|
||||
hasInsurance: boolean;
|
||||
|
||||
@Column({ name: 'insurance_provider', type: 'varchar', length: 100, nullable: true })
|
||||
insuranceProvider?: string;
|
||||
|
||||
@Column({ name: 'insurance_policy_number', type: 'varchar', length: 50, nullable: true })
|
||||
insurancePolicyNumber?: string;
|
||||
|
||||
// Control
|
||||
@Column({ type: 'enum', enum: ['active', 'inactive', 'deceased'], default: 'active' })
|
||||
status: PatientStatus;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
|
||||
@DeleteDateColumn({ name: 'deleted_at', type: 'timestamptz', nullable: true })
|
||||
deletedAt?: Date;
|
||||
|
||||
// Computed property
|
||||
get fullName(): string {
|
||||
return `${this.firstName} ${this.lastName}`;
|
||||
}
|
||||
}
|
||||
5
src/modules/patients/index.ts
Normal file
5
src/modules/patients/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export { PatientsModule, PatientsModuleOptions } from './patients.module';
|
||||
export { Patient, PatientStatus, Gender, BloodType, PatientAddress } from './entities';
|
||||
export { PatientService } from './services';
|
||||
export { PatientController } from './controllers';
|
||||
export { CreatePatientDto, UpdatePatientDto, PatientQueryDto } from './dto';
|
||||
19
src/modules/patients/patients.module.ts
Normal file
19
src/modules/patients/patients.module.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Router } from 'express';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { PatientController } from './controllers';
|
||||
|
||||
export interface PatientsModuleOptions {
|
||||
dataSource: DataSource;
|
||||
basePath?: string;
|
||||
}
|
||||
|
||||
export class PatientsModule {
|
||||
public router: Router;
|
||||
private controller: PatientController;
|
||||
|
||||
constructor(options: PatientsModuleOptions) {
|
||||
const { dataSource, basePath = '/api' } = options;
|
||||
this.controller = new PatientController(dataSource, basePath);
|
||||
this.router = this.controller.router;
|
||||
}
|
||||
}
|
||||
121
src/modules/patients/services/index.ts
Normal file
121
src/modules/patients/services/index.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { DataSource, Repository, ILike } from 'typeorm';
|
||||
import { Patient } from '../entities';
|
||||
import { CreatePatientDto, UpdatePatientDto, PatientQueryDto } from '../dto';
|
||||
|
||||
export class PatientService {
|
||||
private repository: Repository<Patient>;
|
||||
|
||||
constructor(dataSource: DataSource) {
|
||||
this.repository = dataSource.getRepository(Patient);
|
||||
}
|
||||
|
||||
async findAll(tenantId: string, query: PatientQueryDto): Promise<{ data: Patient[]; total: number }> {
|
||||
const { search, status, hasInsurance, page = 1, limit = 20 } = query;
|
||||
|
||||
const whereConditions: any = {
|
||||
tenantId,
|
||||
};
|
||||
|
||||
if (status) {
|
||||
whereConditions.status = status;
|
||||
}
|
||||
|
||||
if (hasInsurance !== undefined) {
|
||||
whereConditions.hasInsurance = hasInsurance;
|
||||
}
|
||||
|
||||
const queryBuilder = this.repository.createQueryBuilder('patient')
|
||||
.where('patient.tenant_id = :tenantId', { tenantId })
|
||||
.andWhere('patient.deleted_at IS NULL');
|
||||
|
||||
if (status) {
|
||||
queryBuilder.andWhere('patient.status = :status', { status });
|
||||
}
|
||||
|
||||
if (hasInsurance !== undefined) {
|
||||
queryBuilder.andWhere('patient.has_insurance = :hasInsurance', { hasInsurance });
|
||||
}
|
||||
|
||||
if (search) {
|
||||
queryBuilder.andWhere(
|
||||
'(patient.first_name ILIKE :search OR patient.last_name ILIKE :search OR patient.mobile ILIKE :search OR patient.email ILIKE :search)',
|
||||
{ search: `%${search}%` }
|
||||
);
|
||||
}
|
||||
|
||||
queryBuilder
|
||||
.orderBy('patient.last_name', 'ASC')
|
||||
.addOrderBy('patient.first_name', 'ASC')
|
||||
.skip((page - 1) * limit)
|
||||
.take(limit);
|
||||
|
||||
const [data, total] = await queryBuilder.getManyAndCount();
|
||||
|
||||
return { data, total };
|
||||
}
|
||||
|
||||
async findById(tenantId: string, id: string): Promise<Patient | null> {
|
||||
return this.repository.findOne({
|
||||
where: { id, tenantId },
|
||||
});
|
||||
}
|
||||
|
||||
async findByMobile(tenantId: string, mobile: string): Promise<Patient | null> {
|
||||
return this.repository.findOne({
|
||||
where: { mobile, tenantId },
|
||||
});
|
||||
}
|
||||
|
||||
async create(tenantId: string, dto: CreatePatientDto): Promise<Patient> {
|
||||
const patient = this.repository.create({
|
||||
...dto,
|
||||
tenantId,
|
||||
dateOfBirth: dto.dateOfBirth ? new Date(dto.dateOfBirth) : undefined,
|
||||
});
|
||||
|
||||
return this.repository.save(patient);
|
||||
}
|
||||
|
||||
async update(tenantId: string, id: string, dto: UpdatePatientDto): Promise<Patient | null> {
|
||||
const patient = await this.findById(tenantId, id);
|
||||
if (!patient) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const updateData: any = { ...dto };
|
||||
if (dto.dateOfBirth) {
|
||||
updateData.dateOfBirth = new Date(dto.dateOfBirth);
|
||||
}
|
||||
|
||||
Object.assign(patient, updateData);
|
||||
return this.repository.save(patient);
|
||||
}
|
||||
|
||||
async softDelete(tenantId: string, id: string): Promise<boolean> {
|
||||
const patient = await this.findById(tenantId, id);
|
||||
if (!patient) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.repository.softDelete(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
async getPatientHistory(tenantId: string, patientId: string): Promise<any> {
|
||||
// Este metodo devolvera el historial completo del paciente
|
||||
// incluyendo citas, consultas, prescripciones, etc.
|
||||
// Por ahora retorna datos basicos
|
||||
const patient = await this.findById(tenantId, patientId);
|
||||
if (!patient) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
patient,
|
||||
appointments: [], // TODO: Integrar con AppointmentService
|
||||
consultations: [], // TODO: Integrar con ConsultationService
|
||||
prescriptions: [], // TODO: Integrar con PrescriptionService
|
||||
};
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user