627 lines
19 KiB
TypeScript
627 lines
19 KiB
TypeScript
import { Component, Input, Output, EventEmitter, OnInit, OnChanges, SimpleChanges } from '@angular/core';
|
|
import { CommonModule } from '@angular/common';
|
|
import { FormsModule } from '@angular/forms';
|
|
import { MedicalRecordService, MedicalRecord, MedicalRecordRequest, VitalSigns, VitalSignsRequest, LabResultRequest } from '../../../../services/medical-record.service';
|
|
import { UserService, PatientProfile } from '../../../../services/user.service';
|
|
import { ModalService } from '../../../../services/modal.service';
|
|
import { LoggerService } from '../../../../services/logger.service';
|
|
|
|
@Component({
|
|
selector: 'app-ehr',
|
|
standalone: true,
|
|
imports: [CommonModule, FormsModule],
|
|
templateUrl: './ehr.component.html',
|
|
styleUrl: './ehr.component.scss'
|
|
})
|
|
export class EhrComponent implements OnInit, OnChanges {
|
|
@Input() doctorId: string | null = null;
|
|
@Input() selectedPatientId: string | null = null;
|
|
@Input() patients: any[] = [];
|
|
@Output() patientSelected = new EventEmitter<string>();
|
|
@Output() dataChanged = new EventEmitter<void>();
|
|
|
|
// State
|
|
showAllRecords = false;
|
|
activeTab: 'records' | 'vitals' | 'labResults' = 'records';
|
|
loading = false;
|
|
error: string | null = null;
|
|
|
|
// Local patient selection for dropdown (separate from Input)
|
|
localSelectedPatientId: string | null = null;
|
|
|
|
// Patient search
|
|
patientSearchQuery: string = '';
|
|
filteredPatients: any[] = [];
|
|
|
|
// Medical Records
|
|
medicalRecords: MedicalRecord[] = [];
|
|
allMedicalRecords: MedicalRecord[] = [];
|
|
showCreateMedicalRecord = false;
|
|
newMedicalRecord: MedicalRecordRequest = {
|
|
patientId: '',
|
|
doctorId: '',
|
|
recordType: 'NOTE',
|
|
title: '',
|
|
content: '',
|
|
diagnosisCode: ''
|
|
};
|
|
|
|
// Vital Signs
|
|
vitalSigns: VitalSigns[] = [];
|
|
latestVitalSigns: VitalSigns | null = null;
|
|
showCreateVitalSigns = false;
|
|
newVitalSigns: VitalSignsRequest = {
|
|
patientId: '',
|
|
temperature: undefined,
|
|
bloodPressureSystolic: undefined,
|
|
bloodPressureDiastolic: undefined,
|
|
heartRate: undefined,
|
|
respiratoryRate: undefined,
|
|
oxygenSaturation: undefined,
|
|
weight: undefined,
|
|
height: undefined,
|
|
notes: ''
|
|
};
|
|
|
|
// Lab Results
|
|
labResults: any[] = [];
|
|
showCreateLabResult = false;
|
|
showUpdateLabResult = false;
|
|
selectedLabResult: any = null;
|
|
newLabResult: LabResultRequest = {
|
|
patientId: undefined as any,
|
|
doctorId: '',
|
|
testName: '',
|
|
resultValue: '',
|
|
status: 'PENDING',
|
|
orderedDate: new Date().toISOString().split('T')[0]
|
|
};
|
|
|
|
// Patient Profile Modal
|
|
selectedPatientProfile: PatientProfile | null = null;
|
|
showPatientProfileModal = false;
|
|
|
|
constructor(
|
|
private medicalRecordService: MedicalRecordService,
|
|
private userService: UserService,
|
|
private modalService: ModalService,
|
|
private logger: LoggerService
|
|
) {}
|
|
|
|
ngOnInit() {
|
|
if (this.doctorId) {
|
|
this.newMedicalRecord.doctorId = this.doctorId;
|
|
this.newLabResult.doctorId = this.doctorId;
|
|
this.loadAllDoctorRecords();
|
|
}
|
|
// Initialize local selection from Input if provided, but don't auto-load
|
|
// This allows parent to set initial selection, but user must explicitly choose
|
|
if (this.selectedPatientId) {
|
|
this.localSelectedPatientId = this.selectedPatientId;
|
|
}
|
|
// Initialize filtered patients
|
|
this.filterPatients();
|
|
}
|
|
|
|
ngOnChanges(changes: SimpleChanges) {
|
|
if (changes['doctorId'] && this.doctorId) {
|
|
this.newMedicalRecord.doctorId = this.doctorId;
|
|
this.newLabResult.doctorId = this.doctorId;
|
|
if (!this.allMedicalRecords.length) {
|
|
this.loadAllDoctorRecords();
|
|
}
|
|
}
|
|
// Update local selection when Input changes, but don't auto-load data
|
|
// User must explicitly select a patient from the dropdown
|
|
if (changes['selectedPatientId']) {
|
|
if (this.selectedPatientId) {
|
|
this.localSelectedPatientId = this.selectedPatientId;
|
|
} else {
|
|
this.localSelectedPatientId = null;
|
|
this.showAllRecords = true;
|
|
}
|
|
}
|
|
// Update filtered patients when patients list changes
|
|
if (changes['patients']) {
|
|
this.filterPatients();
|
|
}
|
|
}
|
|
|
|
async loadAllDoctorRecords() {
|
|
if (!this.doctorId) return;
|
|
try {
|
|
this.loading = true;
|
|
this.allMedicalRecords = await this.medicalRecordService.getMedicalRecordsByDoctorId(this.doctorId);
|
|
} catch (e: any) {
|
|
this.logger.error('Error loading all doctor records:', e);
|
|
this.error = e?.response?.data?.error || 'Failed to load medical records';
|
|
this.allMedicalRecords = [];
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
}
|
|
|
|
async loadPatientData() {
|
|
if (!this.localSelectedPatientId) return;
|
|
await Promise.all([
|
|
this.loadPatientMedicalRecords(),
|
|
this.loadPatientVitalSigns(),
|
|
this.loadPatientLabResults()
|
|
]);
|
|
}
|
|
|
|
async loadPatientMedicalRecords() {
|
|
if (!this.localSelectedPatientId) return;
|
|
try {
|
|
this.loading = true;
|
|
this.medicalRecords = await this.medicalRecordService.getMedicalRecordsByPatientId(this.localSelectedPatientId);
|
|
} catch (e: any) {
|
|
this.logger.error('Error loading medical records:', e);
|
|
this.error = e?.response?.data?.error || 'Failed to load medical records';
|
|
this.medicalRecords = [];
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
}
|
|
|
|
async loadPatientVitalSigns() {
|
|
if (!this.localSelectedPatientId) return;
|
|
try {
|
|
this.loading = true;
|
|
[this.vitalSigns, this.latestVitalSigns] = await Promise.all([
|
|
this.medicalRecordService.getVitalSignsByPatientId(this.localSelectedPatientId),
|
|
this.medicalRecordService.getLatestVitalSignsByPatientId(this.localSelectedPatientId)
|
|
]);
|
|
} catch (e: any) {
|
|
this.logger.error('Error loading vital signs:', e);
|
|
this.error = e?.response?.data?.error || 'Failed to load vital signs';
|
|
this.vitalSigns = [];
|
|
this.latestVitalSigns = null;
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
}
|
|
|
|
async loadPatientLabResults() {
|
|
if (!this.localSelectedPatientId) return;
|
|
try {
|
|
this.loading = true;
|
|
this.labResults = await this.medicalRecordService.getLabResultsByPatientId(this.localSelectedPatientId);
|
|
} catch (e: any) {
|
|
this.logger.error('Error loading lab results:', e);
|
|
this.error = e?.response?.data?.error || 'Failed to load lab results';
|
|
this.labResults = [];
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
}
|
|
|
|
selectPatient(patientId: string | null) {
|
|
if (!patientId) {
|
|
// Clear selection
|
|
this.localSelectedPatientId = null;
|
|
this.selectedPatientId = null;
|
|
this.showAllRecords = true;
|
|
this.patientSelected.emit('');
|
|
// Clear patient-specific data
|
|
this.medicalRecords = [];
|
|
this.vitalSigns = [];
|
|
this.latestVitalSigns = null;
|
|
this.labResults = [];
|
|
return;
|
|
}
|
|
|
|
// User explicitly selected a patient - load their data
|
|
this.localSelectedPatientId = patientId;
|
|
this.selectedPatientId = patientId;
|
|
this.showAllRecords = false;
|
|
this.patientSelected.emit(patientId);
|
|
this.loadPatientData();
|
|
}
|
|
|
|
async toggleRecordsView() {
|
|
this.showAllRecords = !this.showAllRecords;
|
|
if (this.showAllRecords) {
|
|
await this.loadAllDoctorRecords();
|
|
// Clear patient selection when showing all records
|
|
this.localSelectedPatientId = null;
|
|
} else if (this.localSelectedPatientId) {
|
|
// Only load if patient is explicitly selected
|
|
await this.loadPatientData();
|
|
}
|
|
}
|
|
|
|
// Medical Records Methods
|
|
openCreateMedicalRecord() {
|
|
this.showCreateMedicalRecord = !this.showCreateMedicalRecord;
|
|
if (this.showCreateMedicalRecord) {
|
|
this.newMedicalRecord = {
|
|
patientId: this.localSelectedPatientId || '',
|
|
doctorId: this.doctorId || '',
|
|
recordType: 'NOTE',
|
|
title: '',
|
|
content: '',
|
|
diagnosisCode: ''
|
|
};
|
|
}
|
|
}
|
|
|
|
async createMedicalRecord() {
|
|
if (!this.newMedicalRecord.patientId || !this.newMedicalRecord.title || !this.newMedicalRecord.content) {
|
|
this.error = 'Please fill in all required fields';
|
|
return;
|
|
}
|
|
if (!this.newMedicalRecord.doctorId) {
|
|
this.newMedicalRecord.doctorId = this.doctorId || '';
|
|
}
|
|
try {
|
|
this.loading = true;
|
|
const recordId = (this.newMedicalRecord as any).recordId;
|
|
if (recordId) {
|
|
await this.medicalRecordService.updateMedicalRecord(recordId, this.newMedicalRecord);
|
|
} else {
|
|
await this.medicalRecordService.createMedicalRecord(this.newMedicalRecord);
|
|
}
|
|
this.showCreateMedicalRecord = false;
|
|
this.newMedicalRecord = {
|
|
patientId: '',
|
|
doctorId: this.doctorId || '',
|
|
recordType: 'NOTE',
|
|
title: '',
|
|
content: '',
|
|
diagnosisCode: ''
|
|
};
|
|
delete (this.newMedicalRecord as any).recordId;
|
|
await Promise.all([
|
|
this.localSelectedPatientId ? this.loadPatientMedicalRecords() : Promise.resolve(),
|
|
this.loadAllDoctorRecords()
|
|
]);
|
|
this.dataChanged.emit();
|
|
} catch (e: any) {
|
|
this.logger.error('Error creating/updating medical record:', e);
|
|
const recordId = (this.newMedicalRecord as any).recordId;
|
|
const errorMessage = e?.response?.data?.message ||
|
|
e?.response?.data?.error ||
|
|
e?.message ||
|
|
(recordId ? 'Failed to update medical record' : 'Failed to create medical record');
|
|
this.error = errorMessage;
|
|
// Auto-hide error after 5 seconds
|
|
setTimeout(() => {
|
|
this.error = null;
|
|
}, 5000);
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
}
|
|
|
|
editMedicalRecord(record: MedicalRecord) {
|
|
this.newMedicalRecord = {
|
|
patientId: record.patientId,
|
|
doctorId: record.doctorId,
|
|
appointmentId: record.appointmentId,
|
|
recordType: record.recordType,
|
|
title: record.title,
|
|
content: record.content,
|
|
diagnosisCode: record.diagnosisCode
|
|
};
|
|
this.showCreateMedicalRecord = true;
|
|
(this.newMedicalRecord as any).recordId = record.id;
|
|
}
|
|
|
|
async deleteMedicalRecord(recordId: string) {
|
|
const confirmed = await this.modalService.confirm(
|
|
'Are you sure you want to delete this medical record? This action cannot be undone.',
|
|
'Delete Medical Record',
|
|
'Delete',
|
|
'Cancel'
|
|
);
|
|
if (!confirmed) {
|
|
return;
|
|
}
|
|
try {
|
|
this.loading = true;
|
|
await this.medicalRecordService.deleteMedicalRecord(recordId);
|
|
if (this.showAllRecords) {
|
|
await this.loadAllDoctorRecords();
|
|
} else if (this.localSelectedPatientId) {
|
|
await this.loadPatientMedicalRecords();
|
|
}
|
|
this.dataChanged.emit();
|
|
} catch (e: any) {
|
|
this.error = e?.response?.data?.error || 'Failed to delete medical record';
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
}
|
|
|
|
// Vital Signs Methods
|
|
openCreateVitalSigns() {
|
|
this.showCreateVitalSigns = !this.showCreateVitalSigns;
|
|
if (this.showCreateVitalSigns) {
|
|
this.newVitalSigns = {
|
|
patientId: this.localSelectedPatientId || '',
|
|
temperature: undefined,
|
|
bloodPressureSystolic: undefined,
|
|
bloodPressureDiastolic: undefined,
|
|
heartRate: undefined,
|
|
respiratoryRate: undefined,
|
|
oxygenSaturation: undefined,
|
|
weight: undefined,
|
|
height: undefined,
|
|
notes: ''
|
|
};
|
|
}
|
|
}
|
|
|
|
async createVitalSigns() {
|
|
if (!this.newVitalSigns.patientId) {
|
|
this.error = 'Please select a patient';
|
|
return;
|
|
}
|
|
try {
|
|
this.loading = true;
|
|
await this.medicalRecordService.createVitalSigns(this.newVitalSigns);
|
|
this.showCreateVitalSigns = false;
|
|
this.newVitalSigns = {
|
|
patientId: '',
|
|
temperature: undefined,
|
|
bloodPressureSystolic: undefined,
|
|
bloodPressureDiastolic: undefined,
|
|
heartRate: undefined,
|
|
respiratoryRate: undefined,
|
|
oxygenSaturation: undefined,
|
|
weight: undefined,
|
|
height: undefined,
|
|
notes: ''
|
|
};
|
|
if (this.localSelectedPatientId) {
|
|
await this.loadPatientVitalSigns();
|
|
}
|
|
this.dataChanged.emit();
|
|
} catch (e: any) {
|
|
this.error = e?.response?.data?.error || 'Failed to create vital signs';
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
}
|
|
|
|
async deleteVitalSigns(vitalSignsId: string) {
|
|
const confirmed = await this.modalService.confirm(
|
|
'Are you sure you want to delete this vital signs record?',
|
|
'Delete Vital Signs',
|
|
'Delete',
|
|
'Cancel'
|
|
);
|
|
if (!confirmed) {
|
|
return;
|
|
}
|
|
try {
|
|
this.loading = true;
|
|
await this.medicalRecordService.deleteVitalSigns(vitalSignsId);
|
|
if (this.localSelectedPatientId) {
|
|
await this.loadPatientVitalSigns();
|
|
}
|
|
this.dataChanged.emit();
|
|
} catch (e: any) {
|
|
this.error = e?.response?.data?.error || 'Failed to delete vital signs';
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
}
|
|
|
|
// Lab Results Methods
|
|
openCreateLabResult() {
|
|
this.showCreateLabResult = !this.showCreateLabResult;
|
|
this.showUpdateLabResult = false;
|
|
if (this.showCreateLabResult) {
|
|
this.newLabResult = {
|
|
patientId: undefined as any,
|
|
doctorId: this.doctorId || '',
|
|
testName: '',
|
|
resultValue: '',
|
|
status: 'PENDING',
|
|
orderedDate: new Date().toISOString().split('T')[0]
|
|
};
|
|
if (this.localSelectedPatientId) {
|
|
this.newLabResult.patientId = this.localSelectedPatientId as any;
|
|
}
|
|
delete (this.newLabResult as any).labResultId;
|
|
}
|
|
}
|
|
|
|
async createLabResult() {
|
|
// Auto-fill patientId from context if missing
|
|
if (!this.newLabResult.patientId) {
|
|
if (this.localSelectedPatientId) {
|
|
this.newLabResult.patientId = this.localSelectedPatientId as any;
|
|
} else if (Array.isArray(this.patients) && this.patients.length === 1) {
|
|
this.newLabResult.patientId = this.patients[0].id as any;
|
|
}
|
|
}
|
|
|
|
// If patientId is not in known patient IDs, try to resolve from userId
|
|
if (this.newLabResult.patientId && Array.isArray(this.patients) && !this.patients.some(p => p.id === this.newLabResult.patientId)) {
|
|
try {
|
|
const resolved = await this.userService.getPatientIdByUserId(this.newLabResult.patientId as any);
|
|
if (resolved) {
|
|
this.newLabResult.patientId = resolved as any;
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
const missing: string[] = [];
|
|
if (!this.newLabResult.patientId) missing.push('patient');
|
|
if (!this.newLabResult.testName) missing.push('test name');
|
|
if (!this.newLabResult.resultValue) missing.push('result value');
|
|
|
|
if (missing.length) {
|
|
this.error = `Please fill in all required fields: ${missing.join(', ')}`;
|
|
return;
|
|
}
|
|
|
|
try {
|
|
this.loading = true;
|
|
const labResultId = (this.newLabResult as any).labResultId;
|
|
if (labResultId) {
|
|
await this.medicalRecordService.updateLabResult(labResultId, this.newLabResult);
|
|
} else {
|
|
await this.medicalRecordService.createLabResult(this.newLabResult);
|
|
}
|
|
this.showCreateLabResult = false;
|
|
this.showUpdateLabResult = false;
|
|
this.newLabResult = {
|
|
patientId: undefined as any,
|
|
doctorId: this.doctorId || '',
|
|
testName: '',
|
|
resultValue: '',
|
|
status: 'PENDING',
|
|
orderedDate: new Date().toISOString().split('T')[0]
|
|
};
|
|
delete (this.newLabResult as any).labResultId;
|
|
if (this.localSelectedPatientId) {
|
|
await this.loadPatientLabResults();
|
|
}
|
|
this.dataChanged.emit();
|
|
} catch (e: any) {
|
|
this.logger.error('[CreateLabResult] Error:', e);
|
|
let errorMsg = 'Failed to create lab result';
|
|
if (e?.response?.data) {
|
|
if (typeof e.response.data === 'string') {
|
|
errorMsg = e.response.data;
|
|
} else if (e.response.data.error) {
|
|
errorMsg = e.response.data.error;
|
|
} else if (e.response.data.message) {
|
|
errorMsg = e.response.data.message;
|
|
}
|
|
} else if (e?.message) {
|
|
errorMsg = e.message;
|
|
}
|
|
this.error = errorMsg;
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
}
|
|
|
|
editLabResult(labResult: any) {
|
|
this.selectedLabResult = labResult;
|
|
this.newLabResult = {
|
|
patientId: labResult.patientId,
|
|
doctorId: labResult.doctorId,
|
|
testName: labResult.testName,
|
|
resultValue: labResult.resultValue,
|
|
referenceRange: labResult.referenceRange,
|
|
unit: labResult.unit,
|
|
status: labResult.status,
|
|
orderedDate: labResult.orderedDate,
|
|
resultDate: labResult.resultDate,
|
|
notes: labResult.notes
|
|
};
|
|
this.showCreateLabResult = true;
|
|
this.showUpdateLabResult = true;
|
|
(this.newLabResult as any).labResultId = labResult.id;
|
|
}
|
|
|
|
async deleteLabResult(labResultId: string) {
|
|
const confirmed = await this.modalService.confirm(
|
|
'Are you sure you want to delete this lab result?',
|
|
'Delete Lab Result',
|
|
'Delete',
|
|
'Cancel'
|
|
);
|
|
if (!confirmed) {
|
|
return;
|
|
}
|
|
try {
|
|
this.loading = true;
|
|
await this.medicalRecordService.deleteLabResult(labResultId);
|
|
if (this.localSelectedPatientId) {
|
|
await this.loadPatientLabResults();
|
|
}
|
|
this.dataChanged.emit();
|
|
} catch (e: any) {
|
|
this.error = e?.response?.data?.error || 'Failed to delete lab result';
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
}
|
|
|
|
formatDate(dateString: string): string {
|
|
if (!dateString) return 'N/A';
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString();
|
|
}
|
|
|
|
formatDateTime(dateString: string): string {
|
|
if (!dateString) return 'N/A';
|
|
const date = new Date(dateString);
|
|
return date.toLocaleString();
|
|
}
|
|
|
|
getCurrentRecords() {
|
|
return this.showAllRecords ? this.allMedicalRecords : this.medicalRecords;
|
|
}
|
|
|
|
isUpdatingMedicalRecord(): boolean {
|
|
return !!(this.newMedicalRecord as any).recordId;
|
|
}
|
|
|
|
isUpdatingLabResult(): boolean {
|
|
return !!(this.newLabResult as any).labResultId;
|
|
}
|
|
|
|
cancelMedicalRecordForm() {
|
|
this.showCreateMedicalRecord = false;
|
|
delete (this.newMedicalRecord as any).recordId;
|
|
}
|
|
|
|
cancelLabResultForm() {
|
|
this.showCreateLabResult = false;
|
|
this.showUpdateLabResult = false;
|
|
delete (this.newLabResult as any).labResultId;
|
|
}
|
|
|
|
async viewPatientProfile(patientId: string) {
|
|
try {
|
|
this.selectedPatientProfile = await this.userService.getPatientProfileById(patientId);
|
|
this.showPatientProfileModal = true;
|
|
} catch (e: any) {
|
|
this.error = e?.message || 'Failed to load patient profile';
|
|
this.logger.error('Failed to load patient profile:', e);
|
|
}
|
|
}
|
|
|
|
closePatientProfileModal() {
|
|
this.showPatientProfileModal = false;
|
|
this.selectedPatientProfile = null;
|
|
}
|
|
|
|
filterPatients() {
|
|
if (!this.patientSearchQuery || this.patientSearchQuery.trim() === '') {
|
|
this.filteredPatients = this.patients || [];
|
|
return;
|
|
}
|
|
|
|
const query = this.patientSearchQuery.toLowerCase().trim();
|
|
this.filteredPatients = (this.patients || []).filter(patient => {
|
|
const firstName = (patient.firstName || '').toLowerCase();
|
|
const lastName = (patient.lastName || '').toLowerCase();
|
|
const fullName = `${firstName} ${lastName}`.trim();
|
|
const email = (patient.email || '').toLowerCase();
|
|
|
|
return firstName.includes(query) ||
|
|
lastName.includes(query) ||
|
|
fullName.includes(query) ||
|
|
email.includes(query);
|
|
});
|
|
}
|
|
|
|
onPatientSearchChange() {
|
|
this.filterPatients();
|
|
}
|
|
|
|
clearPatientSearch() {
|
|
this.patientSearchQuery = '';
|
|
this.filterPatients();
|
|
}
|
|
}
|