diff --git a/apigateway/src/main/resources/application.yml b/apigateway/src/main/resources/application.yml index 7a70166..d2d94f9 100644 --- a/apigateway/src/main/resources/application.yml +++ b/apigateway/src/main/resources/application.yml @@ -7,15 +7,33 @@ spring: cloud: gateway: + globalcors: + corsConfigurations: + '[/**]': + allowedOriginPatterns: + - "http://localhost:5173" + - "http://localhost:3000" + - "https://*.azurecontainerapps.io" + allowedMethods: + - GET + - POST + - PUT + - DELETE + - OPTIONS + - PATCH + allowedHeaders: "*" + allowCredentials: true + maxAge: 3600 + default-filters: + - DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_UNIQUE routes: - id: iot-service order: -10 - uri: http://iot-service:8082 + uri: lb://iot-service predicates: - - Path=/api/sensordata/** + - Path=/api/iot/** filters: - StripPrefix=0 - - DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_FIRST - id: chatbotbackend uri: lb://chatbotbackend @@ -38,7 +56,6 @@ spring: - Path=/api/** filters: - StripPrefix=0 - - DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_FIRST eureka: client: diff --git a/docker-compose.yml b/docker-compose.yml index eafc52f..6e94ebf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -84,6 +84,7 @@ services: JWT_SECRET: ${JWT_SECRET} EUREKA_CLIENT_SERVICEURL_DEFAULTZONE: ${EUREKA_CLIENT_SERVICEURL_DEFAULTZONE} SPRING_AUTOCONFIGURE_EXCLUDE: org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration + VITALREPORTS_URL: http://vitalreports:8083 iot-service: build: @@ -134,6 +135,8 @@ services: condition: service_healthy environment: EUREKA_CLIENT_SERVICEURL_DEFAULTZONE: http://discoveryserver:8761/eureka + ECG_PYTHON_PATH: python + ECG_SCRIPT_PATH: ecg_predict.py apigateway: build: diff --git a/mainservice/pom.xml b/mainservice/pom.xml index e04b823..2f57ba7 100644 --- a/mainservice/pom.xml +++ b/mainservice/pom.xml @@ -170,4 +170,22 @@ + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + 1.18.34 + + + + + + diff --git a/mainservice/src/main/java/com/example/mainservice/client/VitalReportsClient.java b/mainservice/src/main/java/com/example/mainservice/client/VitalReportsClient.java new file mode 100644 index 0000000..d0ce408 --- /dev/null +++ b/mainservice/src/main/java/com/example/mainservice/client/VitalReportsClient.java @@ -0,0 +1,70 @@ +package com.example.mainservice.client; + +import com.example.mainservice.dto.VitalAssessmentResponseDTO; +import com.example.mainservice.dto.VitalReadingRequestDTO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.http.HttpEntity; +import java.util.Map; + +@Component +public class VitalReportsClient { + + private static final Logger logger = LoggerFactory.getLogger(VitalReportsClient.class); + + private final RestTemplate restTemplate = new RestTemplate(); + + @Value("${vitalreports.url:http://localhost:8081}") + private String vitalReportsUrl; + + public VitalAssessmentResponseDTO evaluateVitals(VitalReadingRequestDTO request) { + String url = vitalReportsUrl + "/api/vital/vitals/evaluate"; + try { + logger.info("Calling VitalReports-AI at {}", url); + ResponseEntity response = + restTemplate.postForEntity(url, request, VitalAssessmentResponseDTO.class); + return response.getBody(); + } catch (Exception e) { + logger.error("Failed to evaluate vitals via VitalReports-AI: {}", e.getMessage()); + VitalAssessmentResponseDTO fallback = new VitalAssessmentResponseDTO(); + fallback.setTriageLevel("UNKNOWN"); + fallback.setVitalStatus(new VitalAssessmentResponseDTO.VitalStatusDTO( + "UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN" + )); + return fallback; + } + } + + public Map analyzeECG(String patientId, MultipartFile datFile, MultipartFile heaFile) { + String url = vitalReportsUrl + "/api/vital/ecg/analyze"; + try { + logger.info("Sending ECG data to VitalReports-AI at {}", url); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("patientId", patientId); + body.add("dat_file", datFile.getResource()); + body.add("hea_file", heaFile.getResource()); + + HttpEntity> requestEntity = new HttpEntity<>(body, headers); + + @SuppressWarnings("unchecked") + ResponseEntity response = restTemplate.postForEntity(url, requestEntity, Map.class); + return response.getBody(); + } catch (Exception e) { + logger.error("Failed to analyze ECG via VitalReports-AI: {}", e.getMessage()); + throw new RuntimeException("ECG Analysis failed in VitalReports service: " + e.getMessage()); + } + } +} diff --git a/mainservice/src/main/java/com/example/mainservice/config/SecurityConfig.java b/mainservice/src/main/java/com/example/mainservice/config/SecurityConfig.java index 2f816cb..44783fe 100644 --- a/mainservice/src/main/java/com/example/mainservice/config/SecurityConfig.java +++ b/mainservice/src/main/java/com/example/mainservice/config/SecurityConfig.java @@ -79,9 +79,6 @@ public CorsConfigurationSource corsConfigurationSource() { public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(AbstractHttpConfigurer::disable) - // Use our corsConfigurationSource instead of disable() — - // this prevents mainservice from emitting its own CORS headers - // which would duplicate the ones the gateway already sets. .cors(cors -> cors.configurationSource(corsConfigurationSource())) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) @@ -91,6 +88,11 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers("/api/auth/**", "/auth/**").permitAll() .requestMatchers("/api/admin/**", "/admin/**").permitAll() .requestMatchers("/api/doctor/**", "/doctor/**").permitAll() + .requestMatchers("/api/doctor-notes/**", "/doctor-notes/**").permitAll() + .requestMatchers("/api/patient/**", "/patient/**").permitAll() + .requestMatchers("/api/pendingdoctor/**", "/pendingdoctor/**").permitAll() + .requestMatchers("/api/dashboard/**", "/dashboard/**").permitAll() + .requestMatchers("/api/vital/**", "/vital/**").permitAll() .requestMatchers(HttpMethod.GET, "/api/payments/pay/**", "/payments/pay/**").permitAll() .requestMatchers("/api/payments/notify", "/payments/notify").permitAll() .requestMatchers("/ws/**", "/ws").permitAll() diff --git a/mainservice/src/main/java/com/example/mainservice/controller/AdminAppointmentController.java b/mainservice/src/main/java/com/example/mainservice/controller/AdminAppointmentController.java index 9b7cd49..64953a9 100644 --- a/mainservice/src/main/java/com/example/mainservice/controller/AdminAppointmentController.java +++ b/mainservice/src/main/java/com/example/mainservice/controller/AdminAppointmentController.java @@ -29,20 +29,23 @@ public List getAllAppointments() { System.out.println("PAID APPOINTMENTS = " + list.size()); - return list.stream().map(a -> new AppointmentDTO( - a.getId(), - a.getAvailability() != null ? a.getAvailability().getId() : null, - a.getDoctor() != null ? a.getDoctor().getName() : "N/A", - a.getDoctor() != null ? a.getDoctor().getSpecialty() : "N/A", - a.getDoctor() != null ? a.getDoctor().getConsultationFee() : 0.0, - a.getAppointmentType().getTypeName(), - a.getPhysicalLocation() != null ? a.getPhysicalLocation() : a.getOnlineLink(), - a.getBookingDate(), - a.getBookingTime(), - a.getReason(), - a.getPaymentStatus().name(), - a.getAppointmentStatus().name() - )).toList(); + return list.stream().map(a -> { + AppointmentDTO dto = new AppointmentDTO(); + dto.setAppointmentId(a.getId()); + dto.setAvailabilityId(a.getAvailability() != null ? a.getAvailability().getId() : null); + dto.setDoctorName(a.getDoctor() != null ? a.getDoctor().getName() : "N/A"); + dto.setSpecialty(a.getDoctor() != null ? a.getDoctor().getSpecialty() : "N/A"); + dto.setConsultationFee(a.getDoctor() != null ? a.getDoctor().getConsultationFee() : 0.0); + dto.setAppointmentType(a.getAppointmentType().getTypeName()); + dto.setLocationOrLink(a.getPhysicalLocation() != null ? a.getPhysicalLocation() : a.getOnlineLink()); + dto.setBookingDate(a.getBookingDate()); + dto.setBookingTime(a.getBookingTime()); + dto.setReason(a.getReason()); + dto.setPaymentStatus(a.getPaymentStatus().name()); + dto.setAppointmentStatus(a.getAppointmentStatus().name()); + dto.setPatientName(a.getPatientName()); + return dto; + }).toList(); } // 🔹 CONFIRM APPOINTMENT @@ -67,4 +70,4 @@ public ResponseEntity confirmAppointment( return ResponseEntity.ok("Confirmed"); } -} \ No newline at end of file +} diff --git a/mainservice/src/main/java/com/example/mainservice/controller/AppointmentController.java b/mainservice/src/main/java/com/example/mainservice/controller/AppointmentController.java index 8abc933..0f7a86f 100644 --- a/mainservice/src/main/java/com/example/mainservice/controller/AppointmentController.java +++ b/mainservice/src/main/java/com/example/mainservice/controller/AppointmentController.java @@ -7,10 +7,10 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.security.Principal; import java.util.List; import java.util.Map; -@CrossOrigin(origins = "http://localhost:5173") @RestController @RequestMapping("/api/appointments") @RequiredArgsConstructor @@ -19,9 +19,10 @@ public class AppointmentController { private final AppointmentService appointmentService; @PostMapping("/book") - public ResponseEntity bookAppointment(@RequestBody AppointmentRequestDTO dto) { + public ResponseEntity bookAppointment(@RequestBody AppointmentRequestDTO dto, Principal principal) { try { - return ResponseEntity.ok(appointmentService.bookAppointment(dto)); + String patientName = principal != null ? principal.getName() : "Unknown"; + return ResponseEntity.ok(appointmentService.bookAppointment(dto, patientName)); } catch (RuntimeException e) { return ResponseEntity.badRequest().body(Map.of("error", e.getMessage())); } catch (Exception e) { diff --git a/mainservice/src/main/java/com/example/mainservice/controller/DoctorAppointmentController.java b/mainservice/src/main/java/com/example/mainservice/controller/DoctorAppointmentController.java new file mode 100644 index 0000000..f466354 --- /dev/null +++ b/mainservice/src/main/java/com/example/mainservice/controller/DoctorAppointmentController.java @@ -0,0 +1,59 @@ +package com.example.mainservice.controller; + +import com.example.mainservice.dto.AppointmentDTO; +import com.example.mainservice.entity.Appointment; +import com.example.mainservice.entity.SpecialDoctor; +import com.example.mainservice.entity.enums.AppointmentStatus; +import com.example.mainservice.repository.AppointmentRepository; +import com.example.mainservice.repository.SpecialDoctorRepository; +import com.example.mainservice.service.AppointmentService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/doctor/appointments") +@RequiredArgsConstructor +public class DoctorAppointmentController { + + private final AppointmentService appointmentService; + private final SpecialDoctorRepository specialDoctorRepository; + private final AppointmentRepository appointmentRepository; + + @GetMapping("/{doctorId}") + public ResponseEntity> getDoctorAppointments(@PathVariable Long doctorId) { + return ResponseEntity.ok(appointmentService.getDoctorAppointments(doctorId)); + } + + @GetMapping("/by-email") + public ResponseEntity getDoctorAppointmentsByEmail(@RequestParam String email) { + SpecialDoctor doctor = specialDoctorRepository.findByEmail(email).orElse(null); + if (doctor == null) { + return ResponseEntity.ok(List.of()); + } + return ResponseEntity.ok(appointmentService.getDoctorAppointments(doctor.getId())); + } + + @PostMapping("/confirm/{id}") + public ResponseEntity confirmAppointment( + @PathVariable Long id, + @RequestParam(required = false) String physicalLocation, + @RequestParam(required = false) String zoomLink + ) { + Appointment a = appointmentRepository.findById(id) + .orElseThrow(() -> new RuntimeException("Appointment not found")); + + if ("Physical".equalsIgnoreCase(a.getAppointmentType().getTypeName())) { + a.setPhysicalLocation(physicalLocation); + } else { + a.setOnlineLink(zoomLink); + } + + a.setAppointmentStatus(AppointmentStatus.CONFIRMED); + appointmentRepository.save(a); + + return ResponseEntity.ok("Confirmed"); + } +} diff --git a/mainservice/src/main/java/com/example/mainservice/controller/DoctorController.java b/mainservice/src/main/java/com/example/mainservice/controller/DoctorController.java index 15b41f1..01c7d6e 100644 --- a/mainservice/src/main/java/com/example/mainservice/controller/DoctorController.java +++ b/mainservice/src/main/java/com/example/mainservice/controller/DoctorController.java @@ -1,9 +1,18 @@ package com.example.mainservice.controller; - +import com.example.mainservice.dto.CriticalAlertDTO; import com.example.mainservice.dto.DoctorDTO; +import com.example.mainservice.dto.DoctorPortalPatientDTO; +import com.example.mainservice.dto.ECGReadingDTO; import com.example.mainservice.entity.Doctor; +import com.example.mainservice.entity.ECGReading; +import com.example.mainservice.entity.EmergencyAlert; +import com.example.mainservice.entity.Patient; +import com.example.mainservice.repository.ECGReadingRepository; +import com.example.mainservice.repository.EmergencyAlertRepository; +import com.example.mainservice.repository.PatientRepo; import com.example.mainservice.service.DoctorService; +import com.example.mainservice.client.VitalReportsClient; import jakarta.validation.Valid; import lombok.AllArgsConstructor; import lombok.Data; @@ -15,25 +24,39 @@ import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.*; +import java.security.Principal; import java.util.HashMap; import java.util.List; import java.util.Map; -@Controller //initialize as a controller -@RestController //initialize rest API s +@Controller +@RestController @RequestMapping("/api/doctor") public class DoctorController { + private static final int ECG_RATIONALE_DB_SAFE_LENGTH = 250; + @Autowired private DoctorService doctorservice; + @Autowired + private PatientRepo patientRepo; + + @Autowired + private EmergencyAlertRepository emergencyAlertRepository; + + @Autowired + private ECGReadingRepository ecgReadingRepository; + + @Autowired + private VitalReportsClient vitalReportsClient; + @PostMapping("/create") - public ResponseEntity createDoctor(@Valid @RequestBody DoctorDTO doctorDto){ + public ResponseEntity createDoctor(@Valid @RequestBody DoctorDTO doctorDto) { try { Doctor doctor = doctorservice.create(doctorDto); return ResponseEntity.status(HttpStatus.CREATED).body(doctor); } catch (RuntimeException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(new ErrorResponse(e.getMessage())); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ErrorResponse(e.getMessage())); } catch (Exception e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(new ErrorResponse("An error occurred during doctor creation: " + e.getMessage())); @@ -41,9 +64,217 @@ public ResponseEntity createDoctor(@Valid @RequestBody DoctorDTO doctorDto){ } @GetMapping("/get") - public List getAllDocters(){ + public List getAllDocters() { + return doctorservice.getDetails(); + } + + @GetMapping("/my-patients") + public ResponseEntity getMyPatients(Principal principal) { + try { + if (principal == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorResponse("Not authenticated")); + } + + Doctor doctor = doctorservice.getDoctorByUsername(principal.getName()); + if (doctor == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Doctor not found")); + } + + List patients = doctorservice.getAssignedPatients(doctor.getId()); + return ResponseEntity.ok(patients); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(new ErrorResponse("Error fetching patients: " + e.getMessage())); + } + } + + @GetMapping("/patients/{doctorId}") + public ResponseEntity getPatientsByDoctorId(@PathVariable Long doctorId) { + try { + Doctor doctor = doctorservice.getDoctorById(doctorId); + if (doctor == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Doctor not found")); + } + + List patients = doctorservice.getAssignedPatients(doctorId); + return ResponseEntity.ok(patients); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(new ErrorResponse("Error fetching patients: " + e.getMessage())); + } + } + + @GetMapping("/alerts/{doctorId}") + public ResponseEntity getAlertsForDoctor(@PathVariable Long doctorId) { + try { + Doctor doctor = doctorservice.getDoctorById(doctorId); + if (doctor == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Doctor not found")); + } + + List assignedPatients = patientRepo.findByAssignedDoctorId(doctorId); + List alerts = assignedPatients.stream() + .flatMap(patient -> emergencyAlertRepository.findByUserId(patient.getId()).stream()) + .sorted((a, b) -> { + if (a.getCreatedAt() == null || b.getCreatedAt() == null) return 0; + return b.getCreatedAt().compareTo(a.getCreatedAt()); + }) + .toList(); + + return ResponseEntity.ok(alerts); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(new ErrorResponse("Error fetching alerts: " + e.getMessage())); + } + } + + @GetMapping("/critical-alerts/{doctorId}") + public ResponseEntity getCriticalAlertsByDoctorId(@PathVariable Long doctorId) { + try { + Doctor doctor = doctorservice.getDoctorById(doctorId); + if (doctor == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Doctor not found")); + } + + List alerts = doctorservice.getCriticalAlerts(doctorId); + return ResponseEntity.ok(alerts); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(new ErrorResponse("Error fetching critical alerts: " + e.getMessage())); + } + } + + @GetMapping("/{doctorId}/patients/ecg-history") + public ResponseEntity getDoctorPatientsECGHistory(@PathVariable Long doctorId) { + try { + Doctor doctor = doctorservice.getDoctorById(doctorId); + if (doctor == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Doctor not found")); + } + + List dtoList = doctorservice.getDoctorPatientsECGHistory(doctorId).stream() + .map(h -> ECGReadingDTO.builder() + .id(h.getId()) + .patientId(h.getPatient().getId()) + .patientName(h.getPatient().getName()) + .prediction(h.getPrediction()) + .probability(h.getProbability()) + .meanHR(h.getMeanHR()) + .sdnn(h.getSdnn()) + .rmssd(h.getRmssd()) + .beats(h.getBeats()) + .status(h.getStatus()) + .rationale(h.getRationale()) + .waveformJson(h.getWaveformJson()) + .recordedAt(h.getRecordedAt()) + .build()) + .toList(); - return doctorservice.getDetails(); + return ResponseEntity.ok(dtoList); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(new ErrorResponse("Error fetching ECG history: " + e.getMessage())); + } + } + + @PostMapping("/ecg/save") + public ResponseEntity saveECGReading(@RequestBody ECGReadingDTO dto) { + try { + Patient patient = patientRepo.findById(dto.getPatientId()) + .orElseThrow(() -> new RuntimeException("Patient not found with id: " + dto.getPatientId())); + + String safeRationale = dto.getRationale(); + if (safeRationale != null && safeRationale.length() > ECG_RATIONALE_DB_SAFE_LENGTH) { + safeRationale = safeRationale.substring(0, ECG_RATIONALE_DB_SAFE_LENGTH); + } + + ECGReading saved = ecgReadingRepository.save(ECGReading.builder() + .patient(patient) + .prediction(dto.getPrediction()) + .probability(dto.getProbability()) + .meanHR(dto.getMeanHR()) + .sdnn(dto.getSdnn()) + .rmssd(dto.getRmssd()) + .beats(dto.getBeats()) + .status(dto.getStatus()) + .rationale(safeRationale) + .waveformJson(dto.getWaveformJson()) + .build()); + + return ResponseEntity.ok(Map.of("message", "ECG reading saved successfully", "id", saved.getId())); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of("error", e.getMessage())); + } + } + + @PostMapping(value = "/ecg/analyze-and-save/{patientId}", consumes = org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE) + public ResponseEntity analyzeAndSaveECG( + @PathVariable Long patientId, + @RequestParam("dat_file") org.springframework.web.multipart.MultipartFile datFile, + @RequestParam("hea_file") org.springframework.web.multipart.MultipartFile heaFile) { + try { + Patient patient = patientRepo.findById(patientId) + .orElseThrow(() -> new RuntimeException("Patient not found with id: " + patientId)); + + Map aiResult = vitalReportsClient.analyzeECG(String.valueOf(patientId), datFile, heaFile); + + if (aiResult == null || aiResult.containsKey("error")) { + throw new RuntimeException("VitalReports-AI Error: " + (aiResult != null ? aiResult.get("error") : "Null response")); + } + + String prediction = (String) aiResult.getOrDefault("prediction", aiResult.getOrDefault("status", "Unknown")); + + double probability = parseDouble(aiResult.get("probability")); + int meanHR = (int) parseDouble(aiResult.get("meanHR")); + double sdnn = parseDouble(aiResult.get("SDNN") != null ? aiResult.get("SDNN") : aiResult.get("sdnn")); + double rmssd = parseDouble(aiResult.get("RMSSD") != null ? aiResult.get("RMSSD") : aiResult.get("rmssd")); + int beats = (int) parseDouble(aiResult.get("beats")); + + String status = (String) aiResult.getOrDefault("status", "Unknown"); + String rationaleRaw = (String) aiResult.get("rationale"); + String safeRationale = rationaleRaw != null ? rationaleRaw : "No rationale provided."; + if (safeRationale.length() > ECG_RATIONALE_DB_SAFE_LENGTH) { + safeRationale = safeRationale.substring(0, ECG_RATIONALE_DB_SAFE_LENGTH); + } + + Object waveformObj = aiResult.get("waveform"); + String waveformJson = "[]"; + if (waveformObj != null) { + waveformJson = new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(waveformObj); + } + + ECGReading saved = ecgReadingRepository.save(ECGReading.builder() + .patient(patient) + .prediction(prediction) + .probability(probability) + .meanHR(meanHR) + .sdnn(sdnn) + .rmssd(rmssd) + .beats(beats) + .status(status) + .rationale(safeRationale) + .waveformJson(waveformJson) + .build()); + + Map response = new HashMap<>(aiResult); + response.put("id", saved.getId()); + response.put("message", "ECG reading analyzed and saved successfully via S2S"); + + return ResponseEntity.ok(response); + + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of("error", e.getMessage())); + } + } + + private double parseDouble(Object obj) { + if (obj == null) return 0.0; + if (obj instanceof Number) return ((Number) obj).doubleValue(); + try { + return Double.parseDouble(obj.toString()); + } catch (NumberFormatException e) { + return 0.0; + } } @DeleteMapping("/delete/{Id}") @@ -57,9 +288,7 @@ public String deleteDoctorByID(@PathVariable Long Id) { } @PutMapping("/update/{Id}") - public ResponseEntity updateDoctorByID( - @PathVariable Long Id, - @RequestBody DoctorDTO doctorDto) { + public ResponseEntity updateDoctorByID(@PathVariable Long Id, @RequestBody DoctorDTO doctorDto) { try { DoctorDTO updatedDoctor = doctorservice.updateDoctor(Id, doctorDto); return ResponseEntity.ok(updatedDoctor); @@ -76,13 +305,9 @@ public ResponseEntity handleValidationExceptions(MethodArgumentNo String errorMessage = error.getDefaultMessage(); errors.put(fieldName, errorMessage); }); - - String errorMessage = errors.values().stream() - .findFirst() - .orElse("Validation failed"); - - return ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(new ErrorResponse(errorMessage)); + + String errorMessage = errors.values().stream().findFirst().orElse("Validation failed"); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ErrorResponse(errorMessage)); } @Data @@ -90,5 +315,4 @@ public ResponseEntity handleValidationExceptions(MethodArgumentNo public static class ErrorResponse { private String message; } - } diff --git a/mainservice/src/main/java/com/example/mainservice/controller/DoctorNotesController.java b/mainservice/src/main/java/com/example/mainservice/controller/DoctorNotesController.java new file mode 100644 index 0000000..543dbf7 --- /dev/null +++ b/mainservice/src/main/java/com/example/mainservice/controller/DoctorNotesController.java @@ -0,0 +1,86 @@ +package com.example.mainservice.controller; + +import com.example.mainservice.entity.Doctor; +import com.example.mainservice.entity.DoctorNote; +import com.example.mainservice.entity.Patient; +import com.example.mainservice.repository.DoctorNoteRepository; +import com.example.mainservice.repository.DoctorRepo; +import com.example.mainservice.repository.PatientRepo; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; + +import java.security.Principal; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/doctor-notes") +@RequiredArgsConstructor +public class DoctorNotesController { + + private final DoctorNoteRepository doctorNoteRepository; + private final DoctorRepo doctorRepo; + private final PatientRepo patientRepo; + + @GetMapping("/patient/{patientId}") + @Transactional(readOnly = true) + public ResponseEntity getNotesByPatient(@PathVariable Long patientId) { + DateTimeFormatter fmt = DateTimeFormatter.ofPattern("MMM d, yyyy h:mm a"); + List> result = doctorNoteRepository.findByPatientIdOrderByCreatedAtDesc(patientId) + .stream() + .map(note -> Map.of( + "id", note.getId(), + "content", note.getContent(), + "author", "Dr. " + note.getDoctor().getName(), + "createdAt", note.getCreatedAt().format(fmt) + )) + .toList(); + return ResponseEntity.ok(result); + } + + @PostMapping("/patient/{patientId}") + @Transactional + public ResponseEntity saveNote( + @PathVariable Long patientId, + @RequestBody Map body, + Principal principal + ) { + if (principal == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("error", "Not authenticated")); + } + + String content = body.get("content"); + if (content == null || content.isBlank()) { + return ResponseEntity.badRequest().body(Map.of("error", "Note content cannot be empty")); + } + + Doctor doctor = doctorRepo.findByUsername(principal.getName()) + .orElseGet(() -> doctorRepo.findByEmail(principal.getName()).orElse(null)); + if (doctor == null) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(Map.of("error", "Only doctors can save notes")); + } + + Patient patient = patientRepo.findById(patientId).orElse(null); + if (patient == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("error", "Patient not found")); + } + + DoctorNote saved = doctorNoteRepository.save(DoctorNote.builder() + .content(content.trim()) + .doctor(doctor) + .patient(patient) + .build()); + + DateTimeFormatter fmt = DateTimeFormatter.ofPattern("MMM d, yyyy h:mm a"); + return ResponseEntity.ok(Map.of( + "id", saved.getId(), + "content", saved.getContent(), + "author", "Dr. " + doctor.getName(), + "createdAt", saved.getCreatedAt().format(fmt) + )); + } +} diff --git a/mainservice/src/main/java/com/example/mainservice/controller/ECGAnalysisController.java b/mainservice/src/main/java/com/example/mainservice/controller/ECGAnalysisController.java new file mode 100644 index 0000000..0a97237 --- /dev/null +++ b/mainservice/src/main/java/com/example/mainservice/controller/ECGAnalysisController.java @@ -0,0 +1,38 @@ +package com.example.mainservice.controller; + +import com.example.mainservice.dto.ECGAnalysisResponseDTO; +import com.example.mainservice.service.ECGAnalysisService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.Map; + +@RestController +@RequestMapping("/api/vital/ecg") +@RequiredArgsConstructor +@Slf4j +public class ECGAnalysisController { + + private final ECGAnalysisService ecgAnalysisService; + + @PostMapping("/analyze") + public ResponseEntity analyze( + @RequestParam("patientId") String patientId, + @RequestParam("dat_file") MultipartFile datFile, + @RequestParam("hea_file") MultipartFile heaFile) { + try { + log.info("Analyzing ECG upload for patient {}", patientId); + ECGAnalysisResponseDTO response = ecgAnalysisService.analyze(datFile, heaFile); + return ResponseEntity.ok(response); + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().body(Map.of("error", e.getMessage())); + } catch (Exception e) { + log.error("ECG analysis failed for patient {}: {}", patientId, e.getMessage(), e); + return ResponseEntity.internalServerError() + .body(Map.of("error", "Failed to analyze ECG upload")); + } + } +} diff --git a/mainservice/src/main/java/com/example/mainservice/controller/PatientController.java b/mainservice/src/main/java/com/example/mainservice/controller/PatientController.java index 0ff5128..ed8b43a 100644 --- a/mainservice/src/main/java/com/example/mainservice/controller/PatientController.java +++ b/mainservice/src/main/java/com/example/mainservice/controller/PatientController.java @@ -1,11 +1,14 @@ package com.example.mainservice.controller; +import com.example.mainservice.dto.ECGReadingDTO; import com.example.mainservice.dto.PatientDTO; -import com.example.mainservice.entity.Patient; +import com.example.mainservice.entity.ECGReading; import com.example.mainservice.entity.EmergencyContact; import com.example.mainservice.entity.Hospital; -import com.example.mainservice.service.PatientService; +import com.example.mainservice.entity.Patient; +import com.example.mainservice.repository.ECGReadingRepository; import com.example.mainservice.service.EmergencyService; +import com.example.mainservice.service.PatientService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -15,6 +18,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; @Controller @RestController @@ -26,6 +30,9 @@ public class PatientController { @Autowired private EmergencyService emergencyService; + @Autowired + private ECGReadingRepository ecgReadingRepository; + @PostMapping("/create") public ResponseEntity> createPatient(@RequestBody PatientDTO patientDto) { Patient patient = patientservice.create(patientDto); @@ -55,9 +62,7 @@ public String deletePatientByID(@PathVariable Long Id) { } @PutMapping("/update/{Id}") - public ResponseEntity updatePatientByID( - @PathVariable Long Id, - @RequestBody PatientDTO patientDto) { + public ResponseEntity updatePatientByID(@PathVariable Long Id, @RequestBody PatientDTO patientDto) { try { PatientDTO updatedPatient = patientservice.updatePatient(Id, patientDto); return ResponseEntity.ok(updatedPatient); @@ -66,13 +71,12 @@ public ResponseEntity updatePatientByID( } } - // NEW: Emergency Panel Endpoint @GetMapping("/emergency-panel/{patientId}") public ResponseEntity> getEmergencyPanelData( @PathVariable Long patientId, @RequestParam(required = false) Double latitude, - @RequestParam(required = false) Double longitude) { - + @RequestParam(required = false) Double longitude + ) { try { Patient patient = patientservice.getPatientByUsername( patientservice.getDetails().stream() @@ -83,40 +87,41 @@ public ResponseEntity> getEmergencyPanelData( ); Map panelData = new HashMap<>(); - - // Get emergency contacts List contacts = emergencyService.getUserContacts(patientId); panelData.put("emergencyContacts", contacts); - // Use patient's stored location if coordinates not provided if (latitude == null && longitude == null) { latitude = patient.getLatitude(); longitude = patient.getLongitude(); } - // Get nearest hospital if location available if (latitude != null && longitude != null) { Hospital nearestHospital = emergencyService.getNearestHospital(latitude, longitude); if (nearestHospital != null) { - Integer eta = emergencyService.calculateETA(latitude, longitude, - nearestHospital.getLatitude(), nearestHospital.getLongitude()); + Integer eta = emergencyService.calculateETA( + latitude, + longitude, + nearestHospital.getLatitude(), + nearestHospital.getLongitude() + ); Map hospitalData = new HashMap<>(); hospitalData.put("hospital", nearestHospital); hospitalData.put("distance", String.format("%.1f km", - emergencyService.calculateDistance(latitude, longitude, - nearestHospital.getLatitude(), nearestHospital.getLongitude()))); + emergencyService.calculateDistance( + latitude, + longitude, + nearestHospital.getLatitude(), + nearestHospital.getLongitude() + ))); hospitalData.put("eta", eta + " mins"); - panelData.put("nearestHospital", hospitalData); } } - // Get active alert if any var activeAlert = emergencyService.getActiveAlert(patientId); panelData.put("activeAlert", activeAlert); - // Add patient info Map patientInfo = new HashMap<>(); patientInfo.put("name", patient.getName()); patientInfo.put("bloodType", patient.getBloodType()); @@ -128,12 +133,10 @@ public ResponseEntity> getEmergencyPanelData( return ResponseEntity.ok(panelData); } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(Map.of("error", e.getMessage())); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of("error", e.getMessage())); } } - // NEW: Get patient by ID endpoint @GetMapping("/get/{Id}") public ResponseEntity getPatientById(@PathVariable Long Id) { try { @@ -146,4 +149,31 @@ public ResponseEntity getPatientById(@PathVariable Long Id) { return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); } } -} \ No newline at end of file + + @GetMapping("/ecg/history/{patientId}") + public ResponseEntity getECGHistory(@PathVariable Long patientId) { + try { + List history = ecgReadingRepository.findByPatientIdOrderByRecordedAtDesc(patientId); + List dtoList = history.stream() + .map(h -> ECGReadingDTO.builder() + .id(h.getId()) + .patientId(h.getPatient().getId()) + .prediction(h.getPrediction()) + .probability(h.getProbability()) + .meanHR(h.getMeanHR()) + .sdnn(h.getSdnn()) + .rmssd(h.getRmssd()) + .beats(h.getBeats()) + .status(h.getStatus()) + .rationale(h.getRationale()) + .waveformJson(h.getWaveformJson()) + .recordedAt(h.getRecordedAt()) + .build()) + .collect(Collectors.toList()); + + return ResponseEntity.ok(dtoList); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of("error", e.getMessage())); + } + } +} diff --git a/mainservice/src/main/java/com/example/mainservice/controller/PaymentController.java b/mainservice/src/main/java/com/example/mainservice/controller/PaymentController.java index 76eb6f1..5542e6a 100644 --- a/mainservice/src/main/java/com/example/mainservice/controller/PaymentController.java +++ b/mainservice/src/main/java/com/example/mainservice/controller/PaymentController.java @@ -5,6 +5,7 @@ import com.example.mainservice.entity.enums.PaymentStatus; import com.example.mainservice.repository.AppointmentRepository; import com.example.mainservice.repository.PaymentRepository; +import com.example.mainservice.service.DoctorAvailabilityService; import com.example.mainservice.service.PaymentService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @@ -18,6 +19,7 @@ public class PaymentController { private final PaymentService paymentService; private final AppointmentRepository appointmentRepository; private final PaymentRepository paymentRepository; + private final DoctorAvailabilityService doctorAvailabilityService; // Build payment form for frontend @GetMapping("/pay/{appointmentId}") @@ -70,10 +72,16 @@ public String notifyPayment( // Set payment and appointment status if ("2".equals(statusCode)) { // 2 = success + if (appointment.getAvailability() != null) { + doctorAvailabilityService.markSlotBooked(appointment.getAvailability().getId()); + } payment.setPaymentStatus(PaymentStatus.SUCCESS); appointment.setPaymentStatus(PaymentStatus.SUCCESS); System.out.println("Payment SUCCESS for appointment " + appointmentId); } else { + if (appointment.getAvailability() != null) { + doctorAvailabilityService.markSlotAvailable(appointment.getAvailability().getId()); + } payment.setPaymentStatus(PaymentStatus.FAILED); appointment.setPaymentStatus(PaymentStatus.FAILED); System.out.println("Payment FAILED for appointment " + appointmentId); @@ -95,4 +103,3 @@ public String notifyPayment( return "ok"; } } - diff --git a/mainservice/src/main/java/com/example/mainservice/controller/ReportController.java b/mainservice/src/main/java/com/example/mainservice/controller/ReportController.java index dda523d..ec5f254 100644 --- a/mainservice/src/main/java/com/example/mainservice/controller/ReportController.java +++ b/mainservice/src/main/java/com/example/mainservice/controller/ReportController.java @@ -26,7 +26,6 @@ public ReportController(ReportService service) { this.service = service; } - // ✅ React: POST /api/patients/{patientId}/reports/upload @PostMapping("/patients/{patientId}/reports/upload") public ResponseEntity upload( @PathVariable Long patientId, @@ -43,7 +42,6 @@ public ResponseEntity upload( response.put("reportName", reportName); return ResponseEntity.ok(response); - } catch (IllegalArgumentException e) { return ResponseEntity.badRequest().body(Map.of("error", e.getMessage())); } catch (Exception e) { @@ -53,7 +51,6 @@ public ResponseEntity upload( } } - // ✅ React: GET /api/patients/{patientId}/reports @GetMapping("/patients/{patientId}/reports") public ResponseEntity> getPatientReports(@PathVariable Long patientId) { try { @@ -64,7 +61,16 @@ public ResponseEntity> getPatientReports(@PathVariable L } } - // ✅ React: GET /api/reports/{id}/download + @GetMapping("/doctor/{doctorId}/patients/reports") + public ResponseEntity> getDoctorPatientsReports(@PathVariable Long doctorId) { + try { + return ResponseEntity.ok(service.getDoctorPatientsReports(doctorId)); + } catch (Exception e) { + logger.error("Failed to fetch doctor's patients reports", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + @GetMapping("/reports/{id}/download") public ResponseEntity downloadReport(@PathVariable Long id) { try { @@ -77,7 +83,6 @@ public ResponseEntity downloadReport(@PathVariable Long id) { .header("Content-Disposition", "attachment; filename=\"" + filename + "\"") .header("Content-Type", report.getFileType()) .body(data); - } catch (Exception e) { logger.error("Download failed", e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); diff --git a/mainservice/src/main/java/com/example/mainservice/controller/VitalSignsController.java b/mainservice/src/main/java/com/example/mainservice/controller/VitalSignsController.java index ba368c5..000ff31 100644 --- a/mainservice/src/main/java/com/example/mainservice/controller/VitalSignsController.java +++ b/mainservice/src/main/java/com/example/mainservice/controller/VitalSignsController.java @@ -8,6 +8,9 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import org.springframework.security.core.context.SecurityContextHolder; +import com.example.mainservice.repository.PatientRepo; +import com.example.mainservice.entity.Patient; import java.util.HashMap; import java.util.List; @@ -24,9 +27,11 @@ public class VitalSignsController { private static final Logger logger = LoggerFactory.getLogger(VitalSignsController.class); private final VitalSignsService service; + private final PatientRepo patientRepo; - public VitalSignsController(VitalSignsService service) { + public VitalSignsController(VitalSignsService service, PatientRepo patientRepo) { this.service = service; + this.patientRepo = patientRepo; } /** @@ -38,9 +43,10 @@ public ResponseEntity submitVitalSigns(@RequestBody VitalSignsDTO dto) { logger.info("Received vital signs submission"); try { - // TODO: Get actual patient ID from authentication context/JWT token - // For now using hardcoded value - Long patientId = 1L; + String email = SecurityContextHolder.getContext().getAuthentication().getName(); + Patient patient = patientRepo.findByEmail(email) + .orElseThrow(() -> new RuntimeException("Patient record not found for email: " + email)); + Long patientId = patient.getId(); VitalSigns saved = service.saveVitalSigns(dto, patientId); diff --git a/mainservice/src/main/java/com/example/mainservice/dto/AppointmentDTO.java b/mainservice/src/main/java/com/example/mainservice/dto/AppointmentDTO.java index 189a247..ed0004d 100644 --- a/mainservice/src/main/java/com/example/mainservice/dto/AppointmentDTO.java +++ b/mainservice/src/main/java/com/example/mainservice/dto/AppointmentDTO.java @@ -21,4 +21,5 @@ public class AppointmentDTO { private String reason; private String paymentStatus; private String appointmentStatus; + private String patientName; } diff --git a/mainservice/src/main/java/com/example/mainservice/dto/CriticalAlertDTO.java b/mainservice/src/main/java/com/example/mainservice/dto/CriticalAlertDTO.java new file mode 100644 index 0000000..650511c --- /dev/null +++ b/mainservice/src/main/java/com/example/mainservice/dto/CriticalAlertDTO.java @@ -0,0 +1,24 @@ +package com.example.mainservice.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CriticalAlertDTO { + private Long patientId; + private String patientName; + private String room; + private String alertTitle; + private String description; + private String severity; + private String currentValue; + private String normalRange; + private LocalDateTime recordedAt; +} diff --git a/mainservice/src/main/java/com/example/mainservice/dto/DoctorPortalPatientDTO.java b/mainservice/src/main/java/com/example/mainservice/dto/DoctorPortalPatientDTO.java new file mode 100644 index 0000000..2b1da2e --- /dev/null +++ b/mainservice/src/main/java/com/example/mainservice/dto/DoctorPortalPatientDTO.java @@ -0,0 +1,33 @@ +package com.example.mainservice.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DoctorPortalPatientDTO { + private Long patientId; + private String patientName; + private String bloodType; + private String contactNo; + private String medicalConditions; + private String city; + private String district; + private String address; + private String gender; + private LocalDate dateOfBirth; + private String room; + private Integer heartRate; + private Double temperature; + private Integer spo2; + private Integer bloodPressureSystolic; + private Integer bloodPressureDiastolic; + private String riskLevel; + private String status; +} diff --git a/mainservice/src/main/java/com/example/mainservice/dto/ECGAnalysisResponseDTO.java b/mainservice/src/main/java/com/example/mainservice/dto/ECGAnalysisResponseDTO.java new file mode 100644 index 0000000..1a2d295 --- /dev/null +++ b/mainservice/src/main/java/com/example/mainservice/dto/ECGAnalysisResponseDTO.java @@ -0,0 +1,25 @@ +package com.example.mainservice.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ECGAnalysisResponseDTO { + private String prediction; + private double probability; + private double meanHR; + private double SDNN; + private double RMSSD; + private int beats; + private String status; + private String rationale; + private int fs; + private List waveform; +} diff --git a/mainservice/src/main/java/com/example/mainservice/dto/ECGReadingDTO.java b/mainservice/src/main/java/com/example/mainservice/dto/ECGReadingDTO.java new file mode 100644 index 0000000..dcaedea --- /dev/null +++ b/mainservice/src/main/java/com/example/mainservice/dto/ECGReadingDTO.java @@ -0,0 +1,28 @@ +package com.example.mainservice.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ECGReadingDTO { + private Long id; + private Long patientId; + private String patientName; + private String prediction; + private double probability; + private int meanHR; + private double sdnn; + private double rmssd; + private int beats; + private String status; + private String rationale; + private String waveformJson; + private LocalDateTime recordedAt; +} diff --git a/mainservice/src/main/java/com/example/mainservice/dto/ReportResponseDTO.java b/mainservice/src/main/java/com/example/mainservice/dto/ReportResponseDTO.java index e601830..4d86ecd 100644 --- a/mainservice/src/main/java/com/example/mainservice/dto/ReportResponseDTO.java +++ b/mainservice/src/main/java/com/example/mainservice/dto/ReportResponseDTO.java @@ -7,6 +7,8 @@ public class ReportResponseDTO { private Long id; private String reportName; private LocalDateTime uploadedAt; + private Long patientId; + private String patientName; public ReportResponseDTO(Long id, String reportName, LocalDateTime uploadedAt) { this.id = id; @@ -14,6 +16,14 @@ public ReportResponseDTO(Long id, String reportName, LocalDateTime uploadedAt) { this.uploadedAt = uploadedAt; } + public ReportResponseDTO(Long id, String reportName, LocalDateTime uploadedAt, Long patientId, String patientName) { + this.id = id; + this.reportName = reportName; + this.uploadedAt = uploadedAt; + this.patientId = patientId; + this.patientName = patientName; + } + public Long getId() { return id; } @@ -25,4 +35,12 @@ public String getReportName() { public LocalDateTime getUploadedAt() { return uploadedAt; } + + public Long getPatientId() { + return patientId; + } + + public String getPatientName() { + return patientName; + } } diff --git a/mainservice/src/main/java/com/example/mainservice/dto/VitalAssessmentResponseDTO.java b/mainservice/src/main/java/com/example/mainservice/dto/VitalAssessmentResponseDTO.java new file mode 100644 index 0000000..b8fc5fc --- /dev/null +++ b/mainservice/src/main/java/com/example/mainservice/dto/VitalAssessmentResponseDTO.java @@ -0,0 +1,26 @@ +package com.example.mainservice.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class VitalAssessmentResponseDTO { + private VitalStatusDTO vitalStatus; + private String triageLevel; + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class VitalStatusDTO { + private String spo2Status; + private String pressureStatus; + private String heartRateStatus; + private String temperatureStatus; + private String bloodSugarStatus; + } +} diff --git a/mainservice/src/main/java/com/example/mainservice/dto/VitalReadingRequestDTO.java b/mainservice/src/main/java/com/example/mainservice/dto/VitalReadingRequestDTO.java new file mode 100644 index 0000000..177717f --- /dev/null +++ b/mainservice/src/main/java/com/example/mainservice/dto/VitalReadingRequestDTO.java @@ -0,0 +1,19 @@ +package com.example.mainservice.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class VitalReadingRequestDTO { + private int spo2; + private int systolicBP; + private int heartRate; + private double temperature; + private double bloodSugar; + private Long timestamp; +} diff --git a/mainservice/src/main/java/com/example/mainservice/entity/Appointment.java b/mainservice/src/main/java/com/example/mainservice/entity/Appointment.java index ce0d0fc..d77b094 100644 --- a/mainservice/src/main/java/com/example/mainservice/entity/Appointment.java +++ b/mainservice/src/main/java/com/example/mainservice/entity/Appointment.java @@ -47,6 +47,8 @@ public class Appointment { private String onlineLink; + private String patientName; + /* ================= PAYMENT ================= */ @Enumerated(EnumType.STRING) diff --git a/mainservice/src/main/java/com/example/mainservice/entity/Doctor.java b/mainservice/src/main/java/com/example/mainservice/entity/Doctor.java index 5d89904..db70e7e 100644 --- a/mainservice/src/main/java/com/example/mainservice/entity/Doctor.java +++ b/mainservice/src/main/java/com/example/mainservice/entity/Doctor.java @@ -44,6 +44,9 @@ public class Doctor { @Column(nullable = false) private String password; + @OneToMany(mappedBy = "doctor", cascade = CascadeType.ALL) + private java.util.List availabilities; + public Doctor() { } diff --git a/mainservice/src/main/java/com/example/mainservice/entity/DoctorAvailability.java b/mainservice/src/main/java/com/example/mainservice/entity/DoctorAvailability.java index a04c454..c80c391 100644 --- a/mainservice/src/main/java/com/example/mainservice/entity/DoctorAvailability.java +++ b/mainservice/src/main/java/com/example/mainservice/entity/DoctorAvailability.java @@ -20,9 +20,15 @@ public class DoctorAvailability { private LocalTime availableTime; + // FK to Doctor entity (used by Doctor.availabilities) @ManyToOne @JoinColumn(name = "doctor_id") - private SpecialDoctor doctor; + private Doctor doctor; + + // FK to SpecialDoctor entity (used by SpecialDoctor.availabilities) + @ManyToOne + @JoinColumn(name = "special_doctor_id") + private SpecialDoctor specialDoctor; @Column(name = "is_booked", nullable = false) private Boolean isBooked = false; // ensures default false diff --git a/mainservice/src/main/java/com/example/mainservice/entity/DoctorNote.java b/mainservice/src/main/java/com/example/mainservice/entity/DoctorNote.java new file mode 100644 index 0000000..c813cc2 --- /dev/null +++ b/mainservice/src/main/java/com/example/mainservice/entity/DoctorNote.java @@ -0,0 +1,39 @@ +package com.example.mainservice.entity; + +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "doctor_notes") +@Builder +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class DoctorNote { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, columnDefinition = "TEXT") + private String content; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "patient_id", nullable = false) + private Patient patient; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "doctor_id", nullable = false) + private Doctor doctor; + + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @PrePersist + protected void onCreate() { + createdAt = LocalDateTime.now(); + } +} diff --git a/mainservice/src/main/java/com/example/mainservice/entity/ECGReading.java b/mainservice/src/main/java/com/example/mainservice/entity/ECGReading.java new file mode 100644 index 0000000..e3b576d --- /dev/null +++ b/mainservice/src/main/java/com/example/mainservice/entity/ECGReading.java @@ -0,0 +1,44 @@ +package com.example.mainservice.entity; + +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@Entity +@Builder +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class ECGReading { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "patient_id", nullable = false) + private Patient patient; + + private String prediction; + private double probability; + private int meanHR; + private double sdnn; + private double rmssd; + private int beats; + private String status; + + @Column(columnDefinition = "LONGTEXT") + private String rationale; + + @Column(columnDefinition = "LONGTEXT") + private String waveformJson; + + @Column(name = "recorded_at") + private LocalDateTime recordedAt; + + @PrePersist + protected void onCreate() { + recordedAt = LocalDateTime.now(); + } +} diff --git a/mainservice/src/main/java/com/example/mainservice/entity/Patient.java b/mainservice/src/main/java/com/example/mainservice/entity/Patient.java index 24147d0..9214ac8 100644 --- a/mainservice/src/main/java/com/example/mainservice/entity/Patient.java +++ b/mainservice/src/main/java/com/example/mainservice/entity/Patient.java @@ -96,6 +96,10 @@ public class Patient { @Column(name = "emergency_notes", length = 1000) private String emergencyNotes; + // Doctor portal: which doctor is assigned to this patient + @Column(name = "assigned_doctor_id") + private Long assignedDoctorId; + @Column(name = "created_at", updatable = false) private LocalDateTime createdAt; diff --git a/mainservice/src/main/java/com/example/mainservice/entity/SpecialDoctor.java b/mainservice/src/main/java/com/example/mainservice/entity/SpecialDoctor.java index 0fb8a25..f7d2771 100644 --- a/mainservice/src/main/java/com/example/mainservice/entity/SpecialDoctor.java +++ b/mainservice/src/main/java/com/example/mainservice/entity/SpecialDoctor.java @@ -41,7 +41,7 @@ public class SpecialDoctor { private String email; - //one doctor have many available date and time - @OneToMany(mappedBy = "doctor", cascade = CascadeType.ALL) + //one SpecialDoctor have many available date and time + @OneToMany(mappedBy = "specialDoctor", cascade = CascadeType.ALL) private List availabilities; } \ No newline at end of file diff --git a/mainservice/src/main/java/com/example/mainservice/entity/VitalSigns.java b/mainservice/src/main/java/com/example/mainservice/entity/VitalSigns.java index 77e7dd6..d709332 100644 --- a/mainservice/src/main/java/com/example/mainservice/entity/VitalSigns.java +++ b/mainservice/src/main/java/com/example/mainservice/entity/VitalSigns.java @@ -40,6 +40,8 @@ public class VitalSigns { // Weight private Double weight; + private String room; + // Date and Time private LocalDateTime measurementDateTime; @@ -53,8 +55,15 @@ public class VitalSigns { // Metadata private LocalDateTime createdAt; + private String triageLevel; + private String spo2Status; + private String pressureStatus; + private String heartRateStatus; + private String temperatureStatus; + private String bloodSugarStatus; + @PrePersist protected void onCreate() { createdAt = LocalDateTime.now(); } -} \ No newline at end of file +} diff --git a/mainservice/src/main/java/com/example/mainservice/repository/AppointmentRepository.java b/mainservice/src/main/java/com/example/mainservice/repository/AppointmentRepository.java index 4d78804..c2c1d11 100644 --- a/mainservice/src/main/java/com/example/mainservice/repository/AppointmentRepository.java +++ b/mainservice/src/main/java/com/example/mainservice/repository/AppointmentRepository.java @@ -10,4 +10,8 @@ public interface AppointmentRepository extends JpaRepository // Fetch only successful payments List findByPaymentStatus(PaymentStatus paymentStatus); + + List findByDoctorIdAndPaymentStatus(Long doctorId, PaymentStatus paymentStatus); + + List findByDoctorId(Long doctorId); } diff --git a/mainservice/src/main/java/com/example/mainservice/repository/DoctorAvailabilityRepository.java b/mainservice/src/main/java/com/example/mainservice/repository/DoctorAvailabilityRepository.java index 275b262..106e5fa 100644 --- a/mainservice/src/main/java/com/example/mainservice/repository/DoctorAvailabilityRepository.java +++ b/mainservice/src/main/java/com/example/mainservice/repository/DoctorAvailabilityRepository.java @@ -9,10 +9,16 @@ public interface DoctorAvailabilityRepository extends JpaRepository { - List findByDoctorIdAndAvailableDateAndIsBookedFalse( + List findBySpecialDoctorIdAndAvailableDateAndIsBookedFalse( Long doctorId, LocalDate date ); - List findByDoctorId(Long doctorId); + List findBySpecialDoctorId(Long doctorId); + + boolean existsBySpecialDoctorIdAndAvailableDateAndAvailableTime( + Long doctorId, + LocalDate availableDate, + java.time.LocalTime availableTime + ); } diff --git a/mainservice/src/main/java/com/example/mainservice/repository/DoctorNoteRepository.java b/mainservice/src/main/java/com/example/mainservice/repository/DoctorNoteRepository.java new file mode 100644 index 0000000..fbfdc7c --- /dev/null +++ b/mainservice/src/main/java/com/example/mainservice/repository/DoctorNoteRepository.java @@ -0,0 +1,12 @@ +package com.example.mainservice.repository; + +import com.example.mainservice.entity.DoctorNote; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface DoctorNoteRepository extends JpaRepository { + List findByPatientIdOrderByCreatedAtDesc(Long patientId); +} diff --git a/mainservice/src/main/java/com/example/mainservice/repository/DoctorRepo.java b/mainservice/src/main/java/com/example/mainservice/repository/DoctorRepo.java index 9f66d1e..8218ff8 100644 --- a/mainservice/src/main/java/com/example/mainservice/repository/DoctorRepo.java +++ b/mainservice/src/main/java/com/example/mainservice/repository/DoctorRepo.java @@ -16,28 +16,16 @@ public interface DoctorRepo extends JpaRepository { boolean existsByEmail(String email); - /* - * Search doctors by name (case-insensitive, partial match) - * Example: "john" will match "Dr. John Smith" - */ List findByNameContainingIgnoreCase(String name); - /** - * Search by doctor registration number (partial match) - */ List findByDoctorRegNoContaining(String doctorRegNo); - /** - * Search by name OR doctor registration number - * This provides the best user experience for searching - */ - List findByNameContainingIgnoreCaseOrDoctorRegNoContaining( - String name, - String doctorRegNo - ); - - /** - * Search by hospital (useful for filtering) - */ + List findByNameContainingIgnoreCaseOrDoctorRegNoContaining(String name, String doctorRegNo); + List findByHospitalContainingIgnoreCase(String hospital); + + // Doctor assignment: find by position for round-robin assignment + List findByPosition(String position); + + List findByPositionContainingIgnoreCase(String position); } diff --git a/mainservice/src/main/java/com/example/mainservice/repository/ECGReadingRepository.java b/mainservice/src/main/java/com/example/mainservice/repository/ECGReadingRepository.java new file mode 100644 index 0000000..1101164 --- /dev/null +++ b/mainservice/src/main/java/com/example/mainservice/repository/ECGReadingRepository.java @@ -0,0 +1,14 @@ +package com.example.mainservice.repository; + +import com.example.mainservice.entity.ECGReading; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface ECGReadingRepository extends JpaRepository { + List findByPatientIdOrderByRecordedAtDesc(Long patientId); + ECGReading findFirstByPatientIdOrderByRecordedAtDesc(Long patientId); + List findByPatient_AssignedDoctorIdOrderByRecordedAtDesc(Long doctorId); +} diff --git a/mainservice/src/main/java/com/example/mainservice/repository/MedicalReportRepository.java b/mainservice/src/main/java/com/example/mainservice/repository/MedicalReportRepository.java index 8c277ff..6dc575e 100644 --- a/mainservice/src/main/java/com/example/mainservice/repository/MedicalReportRepository.java +++ b/mainservice/src/main/java/com/example/mainservice/repository/MedicalReportRepository.java @@ -8,4 +8,6 @@ public interface MedicalReportRepository extends JpaRepository { List findByPatient_IdOrderByUploadedAtDesc(Long patientId); + + List findByPatient_AssignedDoctorIdOrderByUploadedAtDesc(Long doctorId); } diff --git a/mainservice/src/main/java/com/example/mainservice/repository/PatientRepo.java b/mainservice/src/main/java/com/example/mainservice/repository/PatientRepo.java index 24c5399..7141237 100644 --- a/mainservice/src/main/java/com/example/mainservice/repository/PatientRepo.java +++ b/mainservice/src/main/java/com/example/mainservice/repository/PatientRepo.java @@ -20,4 +20,10 @@ public interface PatientRepo extends JpaRepository { // For doctor-side patient search in chat List findByNameContainingIgnoreCaseOrNicNoContainingIgnoreCase(String name, String nicNo); + + // Doctor portal: get all patients assigned to a doctor + List findByAssignedDoctorId(Long doctorId); + + // Doctor assignment: count patients per doctor for load balancing + long countByAssignedDoctorId(Long doctorId); } diff --git a/mainservice/src/main/java/com/example/mainservice/repository/SpecialDoctorRepository.java b/mainservice/src/main/java/com/example/mainservice/repository/SpecialDoctorRepository.java index 42239ea..468fde0 100644 --- a/mainservice/src/main/java/com/example/mainservice/repository/SpecialDoctorRepository.java +++ b/mainservice/src/main/java/com/example/mainservice/repository/SpecialDoctorRepository.java @@ -4,5 +4,8 @@ import com.example.mainservice.entity.SpecialDoctor; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface SpecialDoctorRepository extends JpaRepository { + Optional findByEmail(String email); } diff --git a/mainservice/src/main/java/com/example/mainservice/service/AppointmentService.java b/mainservice/src/main/java/com/example/mainservice/service/AppointmentService.java index 8c448a8..46be8d0 100644 --- a/mainservice/src/main/java/com/example/mainservice/service/AppointmentService.java +++ b/mainservice/src/main/java/com/example/mainservice/service/AppointmentService.java @@ -2,7 +2,10 @@ import com.example.mainservice.dto.AppointmentDTO; import com.example.mainservice.dto.AppointmentRequestDTO; -import com.example.mainservice.entity.*; +import com.example.mainservice.entity.Appointment; +import com.example.mainservice.entity.AppointmentType; +import com.example.mainservice.entity.DoctorAvailability; +import com.example.mainservice.entity.SpecialDoctor; import com.example.mainservice.entity.enums.AppointmentStatus; import com.example.mainservice.entity.enums.PaymentStatus; import com.example.mainservice.repository.AppointmentRepository; @@ -24,91 +27,90 @@ public class AppointmentService { private final SpecialDoctorRepository doctorRepository; private final AppointmentTypeRepository appointmentTypeRepository; - // ============================ - // BOOK APPOINTMENT - // ============================ @Transactional - public AppointmentDTO bookAppointment(AppointmentRequestDTO request) { - + public AppointmentDTO bookAppointment(AppointmentRequestDTO request, String patientName) { SpecialDoctor doctor = doctorRepository.findById(request.getDoctorId()) .orElseThrow(() -> new RuntimeException("Doctor not found")); - AppointmentType appointmentType = appointmentTypeRepository - .findById(request.getAppointmentTypeId()) + AppointmentType appointmentType = appointmentTypeRepository.findById(request.getAppointmentTypeId()) .orElseThrow(() -> new RuntimeException("Appointment type not found")); - DoctorAvailability availableSlot = availabilityService - .getAvailableSlots(request.getDoctorId(), request.getBookingDate()) + DoctorAvailability availableSlot = availabilityService.getAvailableSlots(request.getDoctorId(), request.getBookingDate()) .stream() .filter(slot -> slot.getAvailableTime().equals(request.getBookingTime())) .findFirst() .orElseThrow(() -> new RuntimeException("Selected time slot not available")); - DoctorAvailability bookedSlot = - availabilityService.markSlotBooked(availableSlot.getId()); - Appointment appointment = Appointment.builder() .doctor(doctor) .appointmentType(appointmentType) - .availability(bookedSlot) - .bookingDate(bookedSlot.getAvailableDate()) - .bookingTime(bookedSlot.getAvailableTime()) + .availability(availableSlot) + .bookingDate(availableSlot.getAvailableDate()) + .bookingTime(availableSlot.getAvailableTime()) .reason(request.getReason()) - .paymentStatus(PaymentStatus.PENDING) // Payment not done yet + .patientName(patientName) + .paymentStatus(PaymentStatus.PENDING) .appointmentStatus(AppointmentStatus.PENDING) .build(); - return convertToDTO( - appointmentRepository.save(appointment) - ); + return convertToDTO(appointmentRepository.save(appointment)); } - // ============================ - // USER → ONLY SUCCESS PAYMENTS - // ============================ public List getSuccessfulAppointments() { - return appointmentRepository - .findByPaymentStatus(PaymentStatus.SUCCESS) + return appointmentRepository.findByPaymentStatus(PaymentStatus.SUCCESS) .stream() .map(this::convertToDTO) .collect(Collectors.toList()); } - // ============================ - // ADMIN → ALL APPOINTMENTS - // ============================ public List getAllAppointments() { - return appointmentRepository.findAll() + return appointmentRepository.findAll().stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + public List getDoctorAppointments(Long doctorId) { + return appointmentRepository.findByDoctorIdAndPaymentStatus(doctorId, PaymentStatus.SUCCESS) .stream() .map(this::convertToDTO) .collect(Collectors.toList()); } - // ============================ - // ENTITY → DTO - // ============================ - private AppointmentDTO convertToDTO(Appointment appointment) { - - String locationOrLink = - appointment.getAppointmentType().getTypeName().equalsIgnoreCase("Physical") - ? appointment.getPhysicalLocation() - : appointment.getOnlineLink(); - - return new AppointmentDTO( - appointment.getId(), - appointment.getAvailability() != null - ? appointment.getAvailability().getId() - : null, - appointment.getDoctor().getName(), - appointment.getDoctor().getSpecialty(), - appointment.getDoctor().getConsultationFee(), - appointment.getAppointmentType().getTypeName(), - locationOrLink, - appointment.getBookingDate(), - appointment.getBookingTime(), - appointment.getReason(), - appointment.getPaymentStatus().name(), - appointment.getAppointmentStatus().name() - ); + @Transactional + public AppointmentDTO setMeetingLink(Long appointmentId, String link) { + Appointment appointment = appointmentRepository.findById(appointmentId) + .orElseThrow(() -> new RuntimeException("Appointment not found")); + appointment.setOnlineLink(link); + return convertToDTO(appointmentRepository.save(appointment)); + } + + @Transactional + public AppointmentDTO confirmAppointment(Long appointmentId) { + Appointment appointment = appointmentRepository.findById(appointmentId) + .orElseThrow(() -> new RuntimeException("Appointment not found")); + appointment.setAppointmentStatus(AppointmentStatus.CONFIRMED); + return convertToDTO(appointmentRepository.save(appointment)); + } + + public AppointmentDTO convertToDTO(Appointment appointment) { + String locationOrLink = appointment.getAppointmentType().getTypeName().equalsIgnoreCase("Physical") + ? appointment.getPhysicalLocation() + : appointment.getOnlineLink(); + + AppointmentDTO dto = new AppointmentDTO(); + dto.setAppointmentId(appointment.getId()); + dto.setAvailabilityId(appointment.getAvailability() != null ? appointment.getAvailability().getId() : null); + dto.setDoctorName(appointment.getDoctor().getName()); + dto.setSpecialty(appointment.getDoctor().getSpecialty()); + dto.setConsultationFee(appointment.getDoctor().getConsultationFee()); + dto.setAppointmentType(appointment.getAppointmentType().getTypeName()); + dto.setLocationOrLink(locationOrLink); + dto.setBookingDate(appointment.getBookingDate()); + dto.setBookingTime(appointment.getBookingTime()); + dto.setReason(appointment.getReason()); + dto.setPaymentStatus(appointment.getPaymentStatus().name()); + dto.setAppointmentStatus(appointment.getAppointmentStatus().name()); + dto.setPatientName(appointment.getPatientName()); + return dto; } } diff --git a/mainservice/src/main/java/com/example/mainservice/service/AuthService.java b/mainservice/src/main/java/com/example/mainservice/service/AuthService.java index c631b5b..07f7d97 100644 --- a/mainservice/src/main/java/com/example/mainservice/service/AuthService.java +++ b/mainservice/src/main/java/com/example/mainservice/service/AuthService.java @@ -49,6 +49,7 @@ public class AuthService { private final PasswordEncoder passwordEncoder; private final JwtUtil jwtUtil; private final EmailService emailService; + private final DoctorAssignmentService doctorAssignmentService; public AuthResponse login(LoginRequest loginRequest) { // Validate role is provided @@ -373,6 +374,7 @@ public AuthResponse signup(SignupRequest signupRequest) { .build(); patient = patientRepo.save(patient); + doctorAssignmentService.rebalancePatientsRoundRobin(); CustomUserDetails userDetails = new CustomUserDetails( patient.getId(), diff --git a/mainservice/src/main/java/com/example/mainservice/service/DoctorAssignmentService.java b/mainservice/src/main/java/com/example/mainservice/service/DoctorAssignmentService.java new file mode 100644 index 0000000..db9312c --- /dev/null +++ b/mainservice/src/main/java/com/example/mainservice/service/DoctorAssignmentService.java @@ -0,0 +1,76 @@ +package com.example.mainservice.service; + +import com.example.mainservice.entity.Doctor; +import com.example.mainservice.entity.Patient; +import com.example.mainservice.repository.DoctorRepo; +import com.example.mainservice.repository.PatientRepo; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Comparator; +import java.util.List; + +@Service +@RequiredArgsConstructor +@Slf4j +public class DoctorAssignmentService { + + private final DoctorRepo doctorRepo; + private final PatientRepo patientRepo; + + private List getAssignableDoctors() { + List generalDoctors = doctorRepo.findByPositionContainingIgnoreCase("General"); + if (generalDoctors.isEmpty()) { + generalDoctors = doctorRepo.findAll(); + log.warn("No General Doctors found, using all available doctors for assignment"); + } + return generalDoctors.stream() + .sorted(Comparator.comparing(Doctor::getId)) + .toList(); + } + + public Long assignDoctor() { + List generalDoctors = getAssignableDoctors(); + if (generalDoctors.isEmpty()) { + log.error("No doctors available for patient assignment"); + return null; + } + + Doctor assignedDoctor = generalDoctors.stream() + .min(Comparator.comparingLong(doctor -> patientRepo.countByAssignedDoctorId(doctor.getId()))) + .orElse(generalDoctors.get(0)); + + log.info("Assigned patient to Doctor: {} (ID: {})", assignedDoctor.getName(), assignedDoctor.getId()); + return assignedDoctor.getId(); + } + + public long getPatientCountForDoctor(Long doctorId) { + return patientRepo.countByAssignedDoctorId(doctorId); + } + + @Transactional + public void rebalancePatientsRoundRobin() { + List doctors = getAssignableDoctors(); + if (doctors.isEmpty()) { + log.warn("Skipping patient rebalance because no doctors are available"); + return; + } + + List patients = patientRepo.findAll().stream() + .sorted(Comparator + .comparing(Patient::getCreatedAt, Comparator.nullsLast(Comparator.naturalOrder())) + .thenComparing(Patient::getId)) + .toList(); + + for (int i = 0; i < patients.size(); i++) { + Patient patient = patients.get(i); + Doctor assignedDoctor = doctors.get(i % doctors.size()); + patient.setAssignedDoctorId(assignedDoctor.getId()); + } + + patientRepo.saveAll(patients); + log.info("Rebalanced {} patients across {} doctors using round-robin assignment", patients.size(), doctors.size()); + } +} diff --git a/mainservice/src/main/java/com/example/mainservice/service/DoctorAvailabilityService.java b/mainservice/src/main/java/com/example/mainservice/service/DoctorAvailabilityService.java index be18c11..c5b8a30 100644 --- a/mainservice/src/main/java/com/example/mainservice/service/DoctorAvailabilityService.java +++ b/mainservice/src/main/java/com/example/mainservice/service/DoctorAvailabilityService.java @@ -23,7 +23,7 @@ public class DoctorAvailabilityService { // ✅ GET available slots (return entities, not DTOs) public List getAvailableSlots(Long doctorId, LocalDate date) { - return repository.findByDoctorIdAndAvailableDateAndIsBookedFalse(doctorId, date); + return repository.findBySpecialDoctorIdAndAvailableDateAndIsBookedFalse(doctorId, date); } // ADD multiple slots @@ -32,8 +32,13 @@ public void addAvailability(SaveAvailabilityRequest request) { .orElseThrow(() -> new RuntimeException("Doctor not found")); List slots = request.getTimes().stream() + .filter(time -> !repository.existsBySpecialDoctorIdAndAvailableDateAndAvailableTime( + request.getDoctorId(), + request.getDate(), + time + )) .map(time -> DoctorAvailability.builder() - .doctor(doctor) + .specialDoctor(doctor) .availableDate(request.getDate()) .availableTime(time) .isBooked(false) @@ -72,4 +77,13 @@ public DoctorAvailability markSlotBooked(Long slotId) { slot.setIsBooked(true); return repository.save(slot); } + + @Transactional + public DoctorAvailability markSlotAvailable(Long slotId) { + DoctorAvailability slot = repository.findById(slotId) + .orElseThrow(() -> new RuntimeException("Slot not found")); + + slot.setIsBooked(false); + return repository.save(slot); + } } diff --git a/mainservice/src/main/java/com/example/mainservice/service/DoctorService.java b/mainservice/src/main/java/com/example/mainservice/service/DoctorService.java index 1cbfa70..f80ec97 100644 --- a/mainservice/src/main/java/com/example/mainservice/service/DoctorService.java +++ b/mainservice/src/main/java/com/example/mainservice/service/DoctorService.java @@ -1,24 +1,34 @@ package com.example.mainservice.service; + +import com.example.mainservice.dto.CriticalAlertDTO; import com.example.mainservice.dto.DoctorDTO; +import com.example.mainservice.dto.DoctorPortalPatientDTO; import com.example.mainservice.entity.Doctor; +import com.example.mainservice.entity.Patient; +import com.example.mainservice.entity.VitalSigns; import com.example.mainservice.repository.DoctorRepo; +import com.example.mainservice.repository.ECGReadingRepository; +import com.example.mainservice.repository.PatientRepo; +import com.example.mainservice.repository.VitalSignsRepository; import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import java.util.ArrayList; import java.util.List; -import static java.util.stream.Collectors.toList; - @Service @RequiredArgsConstructor public class DoctorService { private final DoctorRepo doctorrepo; private final PasswordEncoder passwordEncoder; + private final PatientRepo patientRepo; + private final VitalSignsRepository vitalSignsRepository; + private final ECGReadingRepository ecgReadingRepository; + private final DoctorAssignmentService doctorAssignmentService; - public Doctor create(DoctorDTO doctor){ - - Doctor d=Doctor.builder() + public Doctor create(DoctorDTO doctor) { + Doctor d = Doctor.builder() .name(doctor.getName()) .email(doctor.getEmail()) .nicNo(doctor.getNicNo()) @@ -32,12 +42,12 @@ public Doctor create(DoctorDTO doctor){ .username(doctor.getUsername()) .dateOfBirth(doctor.getDateOfBirth()) .build(); - return doctorrepo.save(d); - + Doctor savedDoctor = doctorrepo.save(d); + doctorAssignmentService.rebalancePatientsRoundRobin(); + return savedDoctor; } - public List getDetails(){ - + public List getDetails() { return doctorrepo.findAll().stream().map(d -> DoctorDTO.builder() .Id(d.getId()) .name(d.getName()) @@ -51,33 +61,249 @@ public List getDetails(){ .password(d.getPassword()) .position(d.getPosition()) .username(d.getUsername()) - .dateOfBirth(d.getDateOfBirth()).build()).toList(); + .dateOfBirth(d.getDateOfBirth()) + .build()).toList(); } - public void deleteDoctor(Long Id){ - doctorrepo.deleteById(Id); + public void deleteDoctor(Long id) { + doctorrepo.deleteById(id); + doctorAssignmentService.rebalancePatientsRoundRobin(); } - public DoctorDTO updateDoctor(Long Id,DoctorDTO dto){ - Doctor d = doctorrepo.findById(Id).orElseThrow(); + public DoctorDTO updateDoctor(Long id, DoctorDTO dto) { + Doctor d = doctorrepo.findById(id).orElseThrow(); + + if (dto.getDoctorRegNo() != null) d.setDoctorRegNo(dto.getDoctorRegNo()); + if (dto.getEmail() != null) d.setEmail(dto.getEmail()); + if (dto.getDateOfBirth() != null) d.setDateOfBirth(dto.getDateOfBirth()); + if (dto.getContactNo() != null) d.setContactNo(dto.getContactNo()); + if (dto.getAddress() != null) d.setAddress(dto.getAddress()); + if (dto.getGender() != null) d.setGender(dto.getGender()); + if (dto.getHospital() != null) d.setHospital(dto.getHospital()); + if (dto.getName() != null) d.setName(dto.getName()); + if (dto.getNicNo() != null) d.setNicNo(dto.getNicNo()); + if (dto.getPassword() != null) d.setPassword(dto.getPassword()); + if (dto.getPosition() != null) d.setPosition(dto.getPosition()); + if (dto.getUsername() != null) d.setUsername(dto.getUsername()); - if(dto.getDoctorRegNo()!=null) d.setDoctorRegNo(dto.getDoctorRegNo()); - if(dto.getEmail()!=null) d.setEmail(dto.getEmail()); - if(dto.getDateOfBirth()!=null) d.setDateOfBirth(dto.getDateOfBirth()); - if(dto.getContactNo()!=null) d.setContactNo(dto.getContactNo()); - if(dto.getAddress()!=null) d.setAddress(dto.getAddress()); - if(dto.getGender()!=null) d.setGender(dto.getGender()); - if(dto.getHospital()!=null) d.setHospital(dto.getHospital()); - if(dto.getName()!=null) d.setName(dto.getName()); - if(dto.getNicNo()!=null) d.setNicNo(dto.getNicNo()); - if(dto.getPassword()!=null) d.setPassword(dto.getPassword()); - if(dto.getPosition()!=null) d.setPosition(dto.getPosition()); - if(dto.getUsername()!=null) d.setUsername(dto.getUsername()); + return convertToDTO(doctorrepo.save(d)); + } + + public List getAssignedPatients(Long doctorId) { + return patientRepo.findByAssignedDoctorId(doctorId).stream() + .map(patient -> buildDoctorPortalDTO( + patient, + vitalSignsRepository.findFirstByPatientIdOrderByMeasurementDateTimeDesc(patient.getId()) + )) + .toList(); + } - Doctor updatedDoctor = doctorrepo.save(d); - return convertToDTO(updatedDoctor); + private DoctorPortalPatientDTO buildDoctorPortalDTO(Patient patient, VitalSigns vitals) { + DoctorPortalPatientDTO.DoctorPortalPatientDTOBuilder builder = DoctorPortalPatientDTO.builder() + .patientId(patient.getId()) + .patientName(patient.getName()) + .bloodType(patient.getBloodType()) + .contactNo(patient.getContactNo()) + .medicalConditions(patient.getMedicalConditions()) + .city(patient.getCity()) + .district(patient.getDistrict()) + .address(patient.getAddress()) + .gender(patient.getGender()) + .dateOfBirth(patient.getDateOfBirth()); + if (vitals != null) { + builder.room(vitals.getRoom()) + .heartRate(vitals.getHeartRate()) + .temperature(vitals.getTemperature()) + .spo2(vitals.getSpo2()) + .bloodPressureSystolic(vitals.getBloodPressureSystolic()) + .bloodPressureDiastolic(vitals.getBloodPressureDiastolic()) + .riskLevel(calculateRiskLevel(vitals)) + .status(calculateStatus(vitals)); + } else { + builder.riskLevel("UNKNOWN").status("NO_DATA"); + } + return builder.build(); + } + + private String calculateRiskLevel(VitalSigns vitals) { + int riskScore = 0; + + if (vitals.getHeartRate() != null) { + if (vitals.getHeartRate() < 50 || vitals.getHeartRate() > 120) riskScore += 3; + else if (vitals.getHeartRate() < 60 || vitals.getHeartRate() > 100) riskScore += 1; + } + if (vitals.getSpo2() != null) { + if (vitals.getSpo2() < 90) riskScore += 3; + else if (vitals.getSpo2() < 95) riskScore += 2; + } + if (vitals.getTemperature() != null) { + if (vitals.getTemperature() > 39.0 || vitals.getTemperature() < 35.0) riskScore += 3; + else if (vitals.getTemperature() > 38.0 || vitals.getTemperature() < 36.0) riskScore += 1; + } + if (vitals.getBloodPressureSystolic() != null) { + if (vitals.getBloodPressureSystolic() > 180 || vitals.getBloodPressureSystolic() < 90) riskScore += 3; + else if (vitals.getBloodPressureSystolic() > 140 || vitals.getBloodPressureSystolic() < 100) riskScore += 1; + } + + if (riskScore >= 6) return "CRITICAL"; + if (riskScore >= 4) return "HIGH"; + if (riskScore >= 2) return "MEDIUM"; + return "LOW"; + } + + private String calculateStatus(VitalSigns vitals) { + return switch (calculateRiskLevel(vitals)) { + case "CRITICAL" -> "CRITICAL"; + case "HIGH" -> "MONITORING"; + default -> "STABLE"; + }; + } + + public List getCriticalAlerts(Long doctorId) { + List patients = patientRepo.findByAssignedDoctorId(doctorId); + ArrayList alerts = new ArrayList<>(); + + for (Patient patient : patients) { + VitalSigns vitals = vitalSignsRepository.findFirstByPatientIdOrderByMeasurementDateTimeDesc(patient.getId()); + String roomVal = "Unknown"; + + if (vitals != null) { + roomVal = vitals.getRoom(); + String overallRisk = vitals.getTriageLevel() != null ? vitals.getTriageLevel() : calculateRiskLevel(vitals); + + if ("EMERGENCY".equalsIgnoreCase(overallRisk) || "CRITICAL".equalsIgnoreCase(overallRisk) + || "HIGH".equalsIgnoreCase(overallRisk) || "MEDIUM".equalsIgnoreCase(overallRisk) + || "BAD".equalsIgnoreCase(overallRisk)) { + + int initialAlertsCount = alerts.size(); + + if ("CRITICAL".equalsIgnoreCase(vitals.getSpo2Status()) || "BAD".equalsIgnoreCase(vitals.getSpo2Status())) { + alerts.add(CriticalAlertDTO.builder() + .patientId(patient.getId()).patientName(patient.getName()).room(roomVal) + .alertTitle("SpO2 Alert: " + vitals.getSpo2Status()) + .description("SpO2 level is " + vitals.getSpo2() + "%") + .severity("CRITICAL".equalsIgnoreCase(vitals.getSpo2Status()) ? "CRITICAL" : "HIGH") + .currentValue(vitals.getSpo2() + "%").normalRange("95-100%") + .recordedAt(vitals.getMeasurementDateTime()).build()); + } + + if ("CRITICAL".equalsIgnoreCase(vitals.getHeartRateStatus()) || "BAD".equalsIgnoreCase(vitals.getHeartRateStatus())) { + alerts.add(CriticalAlertDTO.builder() + .patientId(patient.getId()).patientName(patient.getName()).room(roomVal) + .alertTitle("Heart Rate Alert: " + vitals.getHeartRateStatus()) + .description("Heart Rate is " + vitals.getHeartRate() + " bpm") + .severity("CRITICAL".equalsIgnoreCase(vitals.getHeartRateStatus()) ? "CRITICAL" : "HIGH") + .currentValue(vitals.getHeartRate() + " bpm").normalRange("60-100 bpm") + .recordedAt(vitals.getMeasurementDateTime()).build()); + } + + if ("CRITICAL".equalsIgnoreCase(vitals.getPressureStatus()) || "BAD".equalsIgnoreCase(vitals.getPressureStatus())) { + String bpVal = vitals.getBloodPressureSystolic() + "/" + + (vitals.getBloodPressureDiastolic() != null ? vitals.getBloodPressureDiastolic() : "?") + + " mmHg"; + alerts.add(CriticalAlertDTO.builder() + .patientId(patient.getId()).patientName(patient.getName()).room(roomVal) + .alertTitle("Blood Pressure Alert: " + vitals.getPressureStatus()) + .description("Blood Pressure is " + bpVal) + .severity("CRITICAL".equalsIgnoreCase(vitals.getPressureStatus()) ? "CRITICAL" : "HIGH") + .currentValue(bpVal).normalRange("90-120/60-80 mmHg") + .recordedAt(vitals.getMeasurementDateTime()).build()); + } + + if ("CRITICAL".equalsIgnoreCase(vitals.getTemperatureStatus()) || "BAD".equalsIgnoreCase(vitals.getTemperatureStatus())) { + alerts.add(CriticalAlertDTO.builder() + .patientId(patient.getId()).patientName(patient.getName()).room(roomVal) + .alertTitle("Temperature Alert: " + vitals.getTemperatureStatus()) + .description("Temperature is " + vitals.getTemperature() + " C") + .severity("CRITICAL".equalsIgnoreCase(vitals.getTemperatureStatus()) ? "CRITICAL" : "HIGH") + .currentValue(vitals.getTemperature() + " C").normalRange("36.1-37.2C") + .recordedAt(vitals.getMeasurementDateTime()).build()); + } + + if (alerts.size() == initialAlertsCount && ("CRITICAL".equalsIgnoreCase(overallRisk) || "HIGH".equalsIgnoreCase(overallRisk))) { + alerts.add(CriticalAlertDTO.builder() + .patientId(patient.getId()).patientName(patient.getName()).room(roomVal) + .alertTitle("AI Vitals Assessment: " + overallRisk) + .description("VitalReports AI classified combined vitals as " + overallRisk + " risk.") + .severity("CRITICAL".equalsIgnoreCase(overallRisk) ? "CRITICAL" : "HIGH") + .currentValue("View Profile").normalRange("N/A") + .recordedAt(vitals.getMeasurementDateTime()).build()); + } + + } else if (vitals.getTriageLevel() == null && ("CRITICAL".equals(overallRisk) || "HIGH".equals(overallRisk))) { + alerts.add(CriticalAlertDTO.builder() + .patientId(patient.getId()).patientName(patient.getName()).room(roomVal) + .alertTitle("Abnormal Vitals Detected") + .description("Legacy fallback: Vitals triggered high risk.") + .severity(overallRisk) + .currentValue("Check Vitals Tab").normalRange("N/A") + .recordedAt(vitals.getMeasurementDateTime()).build()); + } + } + + var ecg = ecgReadingRepository.findFirstByPatientIdOrderByRecordedAtDesc(patient.getId()); + if (ecg != null && ecg.getPrediction() != null && !"Normal".equalsIgnoreCase(ecg.getPrediction())) { + String prediction = ecg.getPrediction(); + String normalizedPrediction = prediction.toLowerCase(); + String normalizedStatus = ecg.getStatus() == null ? "" : ecg.getStatus().toLowerCase(); + String severity = "HIGH"; + + if ("abnormal".equalsIgnoreCase(prediction) + || "abnormal".equalsIgnoreCase(ecg.getStatus()) + || "inconclusive".equalsIgnoreCase(prediction) + || "inconclusive".equalsIgnoreCase(ecg.getStatus())) { + severity = "CRITICAL"; + } + + if (normalizedPrediction.contains("ventricular") + || normalizedPrediction.contains("tachy") + || normalizedPrediction.contains("fibrillation") + || normalizedPrediction.contains("flutter") + || normalizedPrediction.contains("block") + || normalizedPrediction.contains("stemi") + || normalizedPrediction.contains("ischemia") + || normalizedPrediction.contains("premature") + || normalizedStatus.contains("critical")) { + severity = "CRITICAL"; + } + + alerts.add(CriticalAlertDTO.builder() + .patientId(patient.getId()) + .patientName(patient.getName()) + .room(roomVal) + .alertTitle("ECG Alert: " + prediction) + .description(ecg.getRationale() != null ? ecg.getRationale() : "Abnormal ECG pattern detected by AI.") + .severity(severity) + .currentValue("Confidence: " + String.format("%.1f", ecg.getProbability() * 100) + "%") + .normalRange("Normal Sinus Rhythm") + .recordedAt(ecg.getRecordedAt()) + .build()); + } + } + + alerts.sort((a, b) -> { + int sevA = "CRITICAL".equals(a.getSeverity()) ? 3 : "HIGH".equals(a.getSeverity()) ? 2 : 1; + int sevB = "CRITICAL".equals(b.getSeverity()) ? 3 : "HIGH".equals(b.getSeverity()) ? 2 : 1; + if (sevA != sevB) return sevB - sevA; + if (a.getRecordedAt() != null && b.getRecordedAt() != null) return b.getRecordedAt().compareTo(a.getRecordedAt()); + return 0; + }); + return alerts; + } + + public Doctor getDoctorById(Long id) { + return doctorrepo.findById(id).orElse(null); + } + + public Doctor getDoctorByUsername(String usernameOrEmail) { + return doctorrepo.findByUsername(usernameOrEmail) + .orElseGet(() -> doctorrepo.findByEmail(usernameOrEmail).orElse(null)); + } + + public List getDoctorPatientsECGHistory(Long doctorId) { + return ecgReadingRepository.findByPatient_AssignedDoctorIdOrderByRecordedAtDesc(doctorId); } private DoctorDTO convertToDTO(Doctor doctor) { @@ -95,7 +321,6 @@ private DoctorDTO convertToDTO(Doctor doctor) { dto.setPassword(doctor.getPassword()); dto.setPosition(doctor.getPosition()); dto.setUsername(doctor.getUsername()); - return dto; } } diff --git a/mainservice/src/main/java/com/example/mainservice/service/ECGAnalysisService.java b/mainservice/src/main/java/com/example/mainservice/service/ECGAnalysisService.java new file mode 100644 index 0000000..5a8ac5d --- /dev/null +++ b/mainservice/src/main/java/com/example/mainservice/service/ECGAnalysisService.java @@ -0,0 +1,199 @@ +package com.example.mainservice.service; + +import com.example.mainservice.dto.ECGAnalysisResponseDTO; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +@Service +public class ECGAnalysisService { + + private static final int DEFAULT_FS = 360; + private static final int MAX_WAVEFORM_POINTS = 3000; + + public ECGAnalysisResponseDTO analyze(MultipartFile datFile, MultipartFile heaFile) throws IOException { + if (datFile == null || datFile.isEmpty()) { + throw new IllegalArgumentException("ECG .dat file is required"); + } + if (heaFile == null || heaFile.isEmpty()) { + throw new IllegalArgumentException("ECG .hea file is required"); + } + + int fs = extractSamplingRate(heaFile); + List waveform = extractWaveform(datFile); + List peaks = detectPeaks(waveform, fs); + + double meanHr = calculateMeanHeartRate(peaks, fs); + double sdnn = calculateSdnn(peaks, fs); + double rmssd = calculateRmssd(peaks, fs); + int beats = peaks.size(); + + String status = classifyStatus(meanHr, sdnn, rmssd, beats); + String prediction = status; + double probability = "Normal".equals(status) ? 0.78 : 0.86; + String rationale = buildRationale(meanHr, sdnn, rmssd, beats, fs, status); + + return ECGAnalysisResponseDTO.builder() + .prediction(prediction) + .probability(probability) + .meanHR(round1(meanHr)) + .SDNN(round1(sdnn)) + .RMSSD(round1(rmssd)) + .beats(beats) + .status(status) + .rationale(rationale) + .fs(fs) + .waveform(waveform) + .build(); + } + + private int extractSamplingRate(MultipartFile heaFile) throws IOException { + String header = new String(heaFile.getBytes(), StandardCharsets.UTF_8); + String firstLine = header.lines().findFirst().orElse("").trim(); + if (firstLine.isBlank()) { + return DEFAULT_FS; + } + + String[] parts = firstLine.split("\\s+"); + if (parts.length >= 3) { + try { + String rawFs = parts[2]; + int slashIndex = rawFs.indexOf('/'); + if (slashIndex >= 0) { + rawFs = rawFs.substring(0, slashIndex); + } + return Integer.parseInt(rawFs); + } catch (NumberFormatException ignored) { + return DEFAULT_FS; + } + } + + return DEFAULT_FS; + } + + private List extractWaveform(MultipartFile datFile) throws IOException { + byte[] bytes = datFile.getBytes(); + int limit = Math.min(bytes.length, MAX_WAVEFORM_POINTS); + List waveform = new ArrayList<>(limit); + + for (int i = 0; i < limit; i++) { + waveform.add(bytes[i] / 128.0); + } + + return waveform; + } + + private List detectPeaks(List waveform, int fs) { + List peaks = new ArrayList<>(); + if (waveform.size() < 3) { + return peaks; + } + + double meanAbs = 0.0; + for (double value : waveform) { + meanAbs += Math.abs(value); + } + meanAbs /= waveform.size(); + double threshold = Math.max(0.18, meanAbs * 1.6); + int minDistance = Math.max(1, fs / 3); + int lastPeak = -minDistance; + + for (int i = 1; i < waveform.size() - 1; i++) { + double current = waveform.get(i); + if (current > threshold + && current > waveform.get(i - 1) + && current >= waveform.get(i + 1) + && i - lastPeak >= minDistance) { + peaks.add(i); + lastPeak = i; + } + } + + return peaks; + } + + private double calculateMeanHeartRate(List peaks, int fs) { + if (peaks.size() < 2 || fs <= 0) { + return 0.0; + } + + double totalSeconds = (peaks.get(peaks.size() - 1) - peaks.get(0)) / (double) fs; + if (totalSeconds <= 0) { + return 0.0; + } + + return ((peaks.size() - 1) * 60.0) / totalSeconds; + } + + private double calculateSdnn(List peaks, int fs) { + List rr = rrIntervals(peaks, fs); + if (rr.size() < 2) { + return 0.0; + } + + double mean = rr.stream().mapToDouble(Double::doubleValue).average().orElse(0.0); + double variance = rr.stream() + .mapToDouble(value -> Math.pow(value - mean, 2)) + .average() + .orElse(0.0); + return Math.sqrt(variance); + } + + private double calculateRmssd(List peaks, int fs) { + List rr = rrIntervals(peaks, fs); + if (rr.size() < 2) { + return 0.0; + } + + double sumSquares = 0.0; + int count = 0; + for (int i = 1; i < rr.size(); i++) { + double diff = rr.get(i) - rr.get(i - 1); + sumSquares += diff * diff; + count++; + } + + return count == 0 ? 0.0 : Math.sqrt(sumSquares / count); + } + + private List rrIntervals(List peaks, int fs) { + List rr = new ArrayList<>(); + if (fs <= 0) { + return rr; + } + + for (int i = 1; i < peaks.size(); i++) { + rr.add(((peaks.get(i) - peaks.get(i - 1)) * 1000.0) / fs); + } + return rr; + } + + private String classifyStatus(double meanHr, double sdnn, double rmssd, int beats) { + if (beats < 2) { + return "Inconclusive"; + } + if (meanHr >= 50 && meanHr <= 110 && sdnn <= 180 && rmssd <= 220) { + return "Normal"; + } + return "Abnormal"; + } + + private String buildRationale(double meanHr, double sdnn, double rmssd, int beats, int fs, String status) { + if ("Inconclusive".equals(status)) { + return "The uploaded ECG was accepted, but the signal quality or duration was too limited for a confident automated interpretation."; + } + + return String.format( + "Preliminary heuristic ECG review completed from the uploaded .dat/.hea files at %d Hz. Estimated heart rate %.1f bpm, SDNN %.1f ms, RMSSD %.1f ms, with %d detected beats. This record was classified as %s based on simple rhythm and variability thresholds.", + fs, meanHr, sdnn, rmssd, beats, status + ); + } + + private double round1(double value) { + return Math.round(value * 10.0) / 10.0; + } +} diff --git a/mainservice/src/main/java/com/example/mainservice/service/PatientService.java b/mainservice/src/main/java/com/example/mainservice/service/PatientService.java index dce0fde..0a6c1d3 100644 --- a/mainservice/src/main/java/com/example/mainservice/service/PatientService.java +++ b/mainservice/src/main/java/com/example/mainservice/service/PatientService.java @@ -22,6 +22,7 @@ public class PatientService { private final EmergencyContactRepository emergencyContactRepository; private final LocationService locationService; private final IotDeviceAssignmentService iotDeviceAssignmentService; + private final DoctorAssignmentService doctorAssignmentService; public Patient create(PatientDTO patient) { @@ -66,6 +67,7 @@ public Patient create(PatientDTO patient) { .pastSurgeries(patient.getPastSurgeries()) .emergencyNotes(patient.getEmergencyNotes()) .build(); + p.setAssignedDoctorId(doctorAssignmentService.assignDoctor()); Patient savedPatient = patientrepo.save(p); // Auto-create emergency contact from guardian info diff --git a/mainservice/src/main/java/com/example/mainservice/service/ReportService.java b/mainservice/src/main/java/com/example/mainservice/service/ReportService.java index aa4991a..ca00007 100644 --- a/mainservice/src/main/java/com/example/mainservice/service/ReportService.java +++ b/mainservice/src/main/java/com/example/mainservice/service/ReportService.java @@ -18,4 +18,5 @@ public interface ReportService { List getReportEntitiesByPatient(Long patientId); + List getDoctorPatientsReports(Long doctorId); } diff --git a/mainservice/src/main/java/com/example/mainservice/service/impl/ReportServiceImpl.java b/mainservice/src/main/java/com/example/mainservice/service/impl/ReportServiceImpl.java index 188e9ab..b32d9c8 100644 --- a/mainservice/src/main/java/com/example/mainservice/service/impl/ReportServiceImpl.java +++ b/mainservice/src/main/java/com/example/mainservice/service/impl/ReportServiceImpl.java @@ -28,7 +28,7 @@ public class ReportServiceImpl implements ReportService { private final PatientRepo patientRepo; private final OcrExtractionService ocrService; - public ReportServiceImpl(MedicalReportRepository reportRepository, PatientRepo patientRepo,OcrExtractionService ocrService) { + public ReportServiceImpl(MedicalReportRepository reportRepository, PatientRepo patientRepo, OcrExtractionService ocrService) { this.reportRepository = reportRepository; this.patientRepo = patientRepo; this.ocrService = ocrService; @@ -39,7 +39,6 @@ public List getReportEntitiesByPatient(Long patientId) { return reportRepository.findByPatient_IdOrderByUploadedAtDesc(patientId); } - @Override public void uploadReport(Long patientId, String reportName, MultipartFile file) { logger.info("Upload report for patientId={}, reportName={}, file={}", patientId, reportName, file.getOriginalFilename()); @@ -75,7 +74,6 @@ public void uploadReport(Long patientId, String reportName, MultipartFile file) } reportRepository.save(report); logger.info("Saved report id={} for patientId={}", report.getId(), patientId); - } catch (IOException e) { logger.error("Failed to save file", e); throw new RuntimeException("Failed to save file: " + e.getMessage(), e); @@ -87,7 +85,16 @@ public List getReportsByPatient(Long patientId) { if (patientId == null) throw new IllegalArgumentException("patientId is required"); return reportRepository.findByPatient_IdOrderByUploadedAtDesc(patientId) .stream() - .map(r -> new ReportResponseDTO(r.getId(), r.getReportName(), r.getUploadedAt())) + .map(r -> new ReportResponseDTO(r.getId(), r.getReportName(), r.getUploadedAt(), r.getPatient().getId(), r.getPatient().getName())) + .toList(); + } + + @Override + public List getDoctorPatientsReports(Long doctorId) { + if (doctorId == null) throw new IllegalArgumentException("doctorId is required"); + return reportRepository.findByPatient_AssignedDoctorIdOrderByUploadedAtDesc(doctorId) + .stream() + .map(r -> new ReportResponseDTO(r.getId(), r.getReportName(), r.getUploadedAt(), r.getPatient().getId(), r.getPatient().getName())) .toList(); } diff --git a/mainservice/src/main/java/com/example/mainservice/service/impl/VitalSignsServiceImpl.java b/mainservice/src/main/java/com/example/mainservice/service/impl/VitalSignsServiceImpl.java index ac853b1..bd8f6a0 100644 --- a/mainservice/src/main/java/com/example/mainservice/service/impl/VitalSignsServiceImpl.java +++ b/mainservice/src/main/java/com/example/mainservice/service/impl/VitalSignsServiceImpl.java @@ -1,7 +1,8 @@ -// File: src/main/java/com/example/mainservice/service/impl/VitalSignsServiceImpl.java - package com.example.mainservice.service.impl; +import com.example.mainservice.client.VitalReportsClient; +import com.example.mainservice.dto.VitalAssessmentResponseDTO; +import com.example.mainservice.dto.VitalReadingRequestDTO; import com.example.mainservice.dto.VitalSignsDTO; import com.example.mainservice.entity.VitalSigns; import com.example.mainservice.repository.VitalSignsRepository; @@ -20,9 +21,11 @@ public class VitalSignsServiceImpl implements VitalSignsService { private static final Logger logger = LoggerFactory.getLogger(VitalSignsServiceImpl.class); private final VitalSignsRepository repository; + private final VitalReportsClient vitalReportsClient; - public VitalSignsServiceImpl(VitalSignsRepository repository) { + public VitalSignsServiceImpl(VitalSignsRepository repository, VitalReportsClient vitalReportsClient) { this.repository = repository; + this.vitalReportsClient = vitalReportsClient; } @Override @@ -30,23 +33,30 @@ public VitalSigns saveVitalSigns(VitalSignsDTO dto, Long patientId) { logger.info("Saving vital signs for patient ID: {}", patientId); try { - // Validate that at least one vital sign is provided if (!hasAnyVitalSign(dto)) { throw new IllegalArgumentException("At least one vital sign measurement must be provided"); } - // Validate blood pressure pairs - if ((dto.getBloodPressureSystolic() != null && dto.getBloodPressureDiastolic() == null) || - (dto.getBloodPressureSystolic() == null && dto.getBloodPressureDiastolic() != null)) { + if ((dto.getBloodPressureSystolic() != null && dto.getBloodPressureDiastolic() == null) + || (dto.getBloodPressureSystolic() == null && dto.getBloodPressureDiastolic() != null)) { throw new IllegalArgumentException("Both systolic and diastolic blood pressure values must be provided together"); } - // Parse date and time LocalDate date = LocalDate.parse(dto.getDate()); LocalTime time = LocalTime.parse(dto.getTime()); LocalDateTime measurementDateTime = LocalDateTime.of(date, time); - // Create entity + VitalReadingRequestDTO request = VitalReadingRequestDTO.builder() + .spo2(dto.getSpo2() != null ? dto.getSpo2() : 0) + .systolicBP(dto.getBloodPressureSystolic() != null ? dto.getBloodPressureSystolic() : 0) + .heartRate(dto.getHeartRate() != null ? dto.getHeartRate() : 0) + .temperature(dto.getTemperature() != null ? dto.getTemperature() : 0.0) + .bloodSugar(dto.getBloodSugar() != null ? dto.getBloodSugar() : 0.0) + .timestamp(System.currentTimeMillis()) + .build(); + + VitalAssessmentResponseDTO assessment = vitalReportsClient.evaluateVitals(request); + VitalSigns vitalSigns = new VitalSigns(); vitalSigns.setBloodPressureSystolic(dto.getBloodPressureSystolic()); vitalSigns.setBloodPressureDiastolic(dto.getBloodPressureDiastolic()); @@ -59,10 +69,18 @@ public VitalSigns saveVitalSigns(VitalSignsDTO dto, Long patientId) { vitalSigns.setNotes(dto.getNotes()); vitalSigns.setPatientId(patientId); + if (assessment != null && assessment.getVitalStatus() != null) { + vitalSigns.setTriageLevel(assessment.getTriageLevel()); + vitalSigns.setSpo2Status(assessment.getVitalStatus().getSpo2Status()); + vitalSigns.setPressureStatus(assessment.getVitalStatus().getPressureStatus()); + vitalSigns.setHeartRateStatus(assessment.getVitalStatus().getHeartRateStatus()); + vitalSigns.setTemperatureStatus(assessment.getVitalStatus().getTemperatureStatus()); + vitalSigns.setBloodSugarStatus(assessment.getVitalStatus().getBloodSugarStatus()); + } + VitalSigns saved = repository.save(vitalSigns); logger.info("Vital signs saved successfully with ID: {}", saved.getId()); return saved; - } catch (Exception e) { logger.error("Failed to save vital signs: {}", e.getMessage(), e); throw new RuntimeException("Failed to save vital signs: " + e.getMessage(), e); @@ -99,11 +117,11 @@ public void deleteVitalSigns(Long id) { } private boolean hasAnyVitalSign(VitalSignsDTO dto) { - return dto.getBloodPressureSystolic() != null || - dto.getBloodSugar() != null || - dto.getTemperature() != null || - dto.getHeartRate() != null || - dto.getSpo2() != null || - dto.getWeight() != null; + return dto.getBloodPressureSystolic() != null + || dto.getBloodSugar() != null + || dto.getTemperature() != null + || dto.getHeartRate() != null + || dto.getSpo2() != null + || dto.getWeight() != null; } -} \ No newline at end of file +} diff --git a/mainservice/src/main/resources/application-production.properties b/mainservice/src/main/resources/application-production.properties index a0fcd31..27ebd59 100644 --- a/mainservice/src/main/resources/application-production.properties +++ b/mainservice/src/main/resources/application-production.properties @@ -41,3 +41,5 @@ spring.mail.password=${MAIL_PASSWORD:your-smtp-password} spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.starttls.enable=true + +vitalreports.url=${VITALREPORTS_URL:http://vitalreports:8083} diff --git a/mainservice/src/main/resources/application.properties b/mainservice/src/main/resources/application.properties index ccb09a6..84ce2e1 100644 --- a/mainservice/src/main/resources/application.properties +++ b/mainservice/src/main/resources/application.properties @@ -88,4 +88,6 @@ logging.level.com.example.mainservice=DEBUG logging.level.org.springframework.security=DEBUG logging.level.org.springframework.web=DEBUG -iot.service.url=${IOT_SERVICE_URL:http://iot-service:8082} \ No newline at end of file +iot.service.url=${IOT_SERVICE_URL:http://iot-service:8082} +# VitalReports-AI service URL for S2S ECG analysis +vitalreports.url=${VITALREPORTS_URL:http://vitalreports:8083} diff --git a/vitalReports-AI/Dockerfile b/vitalReports-AI/Dockerfile index 8c9b99e..640974d 100644 --- a/vitalReports-AI/Dockerfile +++ b/vitalReports-AI/Dockerfile @@ -13,6 +13,26 @@ RUN mvn -pl vitalReports-AI -am clean package -DskipTests -q FROM eclipse-temurin:17-jre WORKDIR /app + +# Install Python3 + pip + required packages for ECG ML prediction +# Note: --break-system-packages is required on Ubuntu 24 (Noble) due to PEP 668 +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3 \ + python3-pip \ + python3-numpy \ + && pip3 install --no-cache-dir --break-system-packages tensorflow wfdb scipy \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Create symlink so 'python' resolves to python3 +RUN ln -sf /usr/bin/python3 /usr/bin/python + COPY --from=build /app/vitalReports-AI/target/*.jar app.jar + +# Copy ECG Python script and Keras model into working directory +COPY vitalReports-AI/ecg_predict.py ecg_predict.py +COPY vitalReports-AI/ecg_cnn_mitbih.keras ecg_cnn_mitbih.keras + EXPOSE 8083 -ENTRYPOINT ["java","-jar","app.jar"] +ENV ECG_PYTHON_PATH=python +ENV ECG_SCRIPT_PATH=ecg_predict.py +ENTRYPOINT ["java","-jar","app.jar"] \ No newline at end of file diff --git a/vitalReports-AI/ecg_cnn_mitbih.keras b/vitalReports-AI/ecg_cnn_mitbih.keras new file mode 100644 index 0000000..34585ec Binary files /dev/null and b/vitalReports-AI/ecg_cnn_mitbih.keras differ diff --git a/vitalReports-AI/ecg_predict.py b/vitalReports-AI/ecg_predict.py new file mode 100644 index 0000000..dc5c955 --- /dev/null +++ b/vitalReports-AI/ecg_predict.py @@ -0,0 +1,167 @@ +import sys +import os +import json +import numpy as np +import tensorflow as tf + +WINDOW_SIZE = 180 +FS = 360 + +# ─── Load trained model ─── +script_dir = os.path.dirname(os.path.abspath(__file__)) +model_path = os.path.join(script_dir, "ecg_cnn_mitbih.keras") +model = tf.keras.models.load_model(model_path) + + +def detect_r_peaks(ecg, fs): + """Detect R-peaks using simple peak detection (no annotation file needed).""" + from scipy.signal import find_peaks + + # Normalize signal for peak detection + ecg_norm = (ecg - np.mean(ecg)) / np.std(ecg) + + # Minimum distance between peaks: at least 200ms apart (300 bpm max) + min_distance = int(0.2 * fs) + + # Find peaks with minimum height and distance + peaks, properties = find_peaks( + ecg_norm, + height=0.5, # peaks must be above 0.5 std devs + distance=min_distance, # at least 200ms apart + prominence=0.5 # peaks must be prominent + ) + + # If too few peaks found, try with lower thresholds + if len(peaks) < 5: + peaks, properties = find_peaks( + ecg_norm, + height=0.3, + distance=min_distance, + prominence=0.3 + ) + + return peaks + + +def parse_ecg_files(record_path): + """Parse .dat + .hea files using wfdb and extract beats using R-peak detection.""" + import wfdb + + record = wfdb.rdrecord(record_path) + ecg = record.p_signal[:, 0] # MLII lead (first channel) + fs = record.fs + + # Try to load annotation file, fall back to algorithmic R-peak detection + try: + annotation = wfdb.rdann(record_path, "atr") + r_peaks = annotation.sample + except Exception: + # No annotation file — detect R-peaks algorithmically + r_peaks = detect_r_peaks(ecg, fs) + + beats = [] + + for r_peak in r_peaks: + start = r_peak - WINDOW_SIZE // 2 + end = r_peak + WINDOW_SIZE // 2 + + if start < 0 or end > len(ecg): + continue + + beat = ecg[start:end] + beats.append(beat) + + return ecg, fs, beats, r_peaks + + +def analyze_from_files(record_path): + """Full analysis: parse files, run CNN on each beat, return results.""" + ecg, fs, beats, r_peaks = parse_ecg_files(record_path) + + if len(beats) == 0: + return { + "prediction": "No beats detected", + "probability": 0.0, + "waveform": ecg[:3600].tolist(), + "fs": int(fs), + "meanHR": 0, + "sdnn": 0, + "rmssd": 0, + "beats": 0, + "status": "Unable to analyze", + "rationale": "No valid beats were detected in the uploaded ECG." + } + + # Normalize and reshape beats for CNN + X = np.array(beats) + max_val = np.max(np.abs(X)) + if max_val > 0: + X = X / max_val + X = X.reshape(-1, WINDOW_SIZE, 1) + + # Predict all beats + predictions = model.predict(X, verbose=0) + abnormal_count = int(np.sum(predictions > 0.5)) + normal_count = len(predictions) - abnormal_count + avg_prob = float(np.mean(predictions)) + + # HRV metrics from R-peaks + rr_intervals = np.diff(r_peaks) / fs * 1000 # in ms + mean_hr = int(60000 / np.mean(rr_intervals)) if len(rr_intervals) > 0 else 0 + sdnn = round(float(np.std(rr_intervals)), 2) if len(rr_intervals) > 1 else 0 + rmssd = round(float(np.sqrt(np.mean(np.diff(rr_intervals) ** 2))), 2) if len(rr_intervals) > 1 else 0 + + # Overall status + if abnormal_count / len(predictions) > 0.3: + status = "Abnormal" + rationale = (f"CNN detected {abnormal_count} abnormal beats out of {len(predictions)} total. " + f"Mean probability: {avg_prob:.2f}. Possible arrhythmia — further review recommended.") + else: + status = "Normal" + rationale = (f"CNN classified {normal_count}/{len(predictions)} beats as normal. " + f"Mean probability: {avg_prob:.2f}. ECG appears within normal limits.") + + # Return only first 10 seconds of waveform for graph display + waveform_samples = min(len(ecg), int(fs * 10)) + + return { + "prediction": status, + "probability": round(avg_prob, 4), + "waveform": ecg[:waveform_samples].tolist(), + "fs": int(fs), + "meanHR": mean_hr, + "sdnn": sdnn, + "rmssd": rmssd, + "beats": len(predictions), + "status": status, + "rationale": rationale + } + + +def analyze_from_signal(signal_json): + """Original mode: analyze a single 180-sample signal array.""" + input_data = json.loads(signal_json) + signal = np.array(input_data["signal"]) + signal = signal / np.max(np.abs(signal)) + signal = signal.reshape(1, WINDOW_SIZE, 1) + + prob = float(model.predict(signal, verbose=0)[0][0]) + label = "Abnormal" if prob > 0.5 else "Normal" + + return { + "prediction": label, + "probability": round(prob, 4) + } + + +if __name__ == "__main__": + mode = sys.argv[1] + + if mode == "file": + record_path = sys.argv[2] + result = analyze_from_files(record_path) + else: + json_str = sys.argv[2] + result = analyze_from_signal(json_str) + + print(json.dumps(result)) diff --git a/vitalReports-AI/src/main/java/com/example/vitalReports/controller/ECGController.java b/vitalReports-AI/src/main/java/com/example/vitalReports/controller/ECGController.java new file mode 100644 index 0000000..c6c24e5 --- /dev/null +++ b/vitalReports-AI/src/main/java/com/example/vitalReports/controller/ECGController.java @@ -0,0 +1,69 @@ +package com.example.vitalReports.controller; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import com.example.vitalReports.domain.model.ECGPredictionResponse; +import com.example.vitalReports.service.ECGPredictionService; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/vital/ecg") +public class ECGController { + + private final ECGPredictionService ecgPredictionService; + + public ECGController(ECGPredictionService ecgPredictionService) { + this.ecgPredictionService = ecgPredictionService; + } + + /** + * File upload endpoint: accepts .dat + .hea files for full ECG analysis. + */ + @PostMapping("/analyze") + public ResponseEntity analyzeECG( + @RequestParam("dat_file") MultipartFile datFile, + @RequestParam("hea_file") MultipartFile heaFile) { + + if (datFile.isEmpty() || heaFile.isEmpty()) { + return ResponseEntity.badRequest().body(Map.of("error", "Both .dat and .hea files are required")); + } + + try { + ECGPredictionResponse result = ecgPredictionService.analyzeFiles(datFile, heaFile); + return ResponseEntity.ok(result); + } catch (Exception e) { + System.err.println("[ECG ERROR] " + e.getMessage()); + e.printStackTrace(); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(Map.of("error", "ECG analysis failed: " + e.getMessage())); + } + } + + /** + * Signal-based endpoint: accepts a 180-sample signal array. + */ + @PostMapping("/predict") + public ResponseEntity predictECG( + @RequestBody Map> body) { + + List signal = body.get("signal"); + + if (signal == null || signal.isEmpty()) { + return ResponseEntity.badRequest().body(Map.of("error", "Signal array is required")); + } + + try { + ECGPredictionResponse result = ecgPredictionService.predictECG(signal); + return ResponseEntity.ok(result); + } catch (Exception e) { + System.err.println("[ECG ERROR] " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(Map.of("error", "ECG prediction failed: " + e.getMessage())); + } + } +} diff --git a/vitalReports-AI/src/main/java/com/example/vitalReports/domain/model/ECGPredictionResponse.java b/vitalReports-AI/src/main/java/com/example/vitalReports/domain/model/ECGPredictionResponse.java new file mode 100644 index 0000000..bb88da0 --- /dev/null +++ b/vitalReports-AI/src/main/java/com/example/vitalReports/domain/model/ECGPredictionResponse.java @@ -0,0 +1,102 @@ +package com.example.vitalReports.domain.model; + +import java.util.List; + +public class ECGPredictionResponse { + + private String prediction; + private double probability; + private List waveform; + private int fs; + private int meanHR; + private double SDNN; + private double RMSSD; + private int beats; + private String status; + private String rationale; + + public ECGPredictionResponse() { + } + + // Getters and Setters + + public String getPrediction() { + return prediction; + } + + public void setPrediction(String prediction) { + this.prediction = prediction; + } + + public double getProbability() { + return probability; + } + + public void setProbability(double probability) { + this.probability = probability; + } + + public List getWaveform() { + return waveform; + } + + public void setWaveform(List waveform) { + this.waveform = waveform; + } + + public int getFs() { + return fs; + } + + public void setFs(int fs) { + this.fs = fs; + } + + public int getMeanHR() { + return meanHR; + } + + public void setMeanHR(int meanHR) { + this.meanHR = meanHR; + } + + public double getSDNN() { + return SDNN; + } + + public void setSDNN(double SDNN) { + this.SDNN = SDNN; + } + + public double getRMSSD() { + return RMSSD; + } + + public void setRMSSD(double RMSSD) { + this.RMSSD = RMSSD; + } + + public int getBeats() { + return beats; + } + + public void setBeats(int beats) { + this.beats = beats; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getRationale() { + return rationale; + } + + public void setRationale(String rationale) { + this.rationale = rationale; + } +} diff --git a/vitalReports-AI/src/main/java/com/example/vitalReports/service/ECGPredictionService.java b/vitalReports-AI/src/main/java/com/example/vitalReports/service/ECGPredictionService.java new file mode 100644 index 0000000..d704086 --- /dev/null +++ b/vitalReports-AI/src/main/java/com/example/vitalReports/service/ECGPredictionService.java @@ -0,0 +1,164 @@ +package com.example.vitalReports.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import com.example.vitalReports.domain.model.ECGPredictionResponse; + +import java.io.BufferedReader; +import java.io.File; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +public class ECGPredictionService { + + @Value("${ecg.python.path:python}") + private String pythonPath; + + @Value("${ecg.script.path:ecg_predict.py}") + private String scriptPath; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + /** + * Resolve the script path to an absolute path. + */ + private String resolveScriptPath() { + File scriptFile = new File(scriptPath); + if (scriptFile.isAbsolute() && scriptFile.exists()) { + return scriptFile.getAbsolutePath(); + } + + File cwdFile = new File(System.getProperty("user.dir"), scriptPath); + if (cwdFile.exists()) { + return cwdFile.getAbsolutePath(); + } + + String[] searchPaths = { ".", "..", System.getProperty("user.dir") }; + for (String base : searchPaths) { + File f = new File(base, scriptPath); + if (f.exists()) { + return f.getAbsolutePath(); + } + } + + return scriptPath; + } + + private File getWorkingDirectory() { + String resolved = resolveScriptPath(); + return new File(resolved).getParentFile(); + } + + /** + * Analyze ECG from uploaded .dat + .hea files. + */ + public ECGPredictionResponse analyzeFiles(MultipartFile datFile, MultipartFile heaFile) throws Exception { + + Path tempDir = Files.createTempDirectory("ecg_upload_"); + + String heaName = heaFile.getOriginalFilename(); + String recordName = heaName.substring(0, heaName.lastIndexOf(".")); + + Path datPath = tempDir.resolve(recordName + ".dat"); + Path heaPath = tempDir.resolve(recordName + ".hea"); + + datFile.transferTo(datPath.toFile()); + heaFile.transferTo(heaPath.toFile()); + + String recordPath = tempDir.resolve(recordName).toString(); + + String absoluteScriptPath = resolveScriptPath(); + File workDir = getWorkingDirectory(); + + System.out.println("[ECG] Script path: " + absoluteScriptPath); + System.out.println("[ECG] Working dir: " + workDir); + System.out.println("[ECG] Record path: " + recordPath); + + ProcessBuilder pb = new ProcessBuilder(pythonPath, absoluteScriptPath, "file", recordPath); + pb.directory(workDir); + pb.redirectErrorStream(false); + + Process process = pb.start(); + + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + StringBuilder output = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + output.append(line); + } + + BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream())); + StringBuilder errorOutput = new StringBuilder(); + String errorLine; + while ((errorLine = errorReader.readLine()) != null) { + errorOutput.append(errorLine).append("\n"); + } + + int exitCode = process.waitFor(); + + System.out.println("[ECG] Exit code: " + exitCode); + System.out.println("[ECG] Output: " + output); + if (!errorOutput.isEmpty()) { + System.out.println("[ECG] Stderr: " + errorOutput); + } + + // Cleanup + try { + Files.deleteIfExists(datPath); + Files.deleteIfExists(heaPath); + Files.deleteIfExists(tempDir); + } catch (Exception ignored) { + } + + if (exitCode != 0 || output.isEmpty()) { + throw new RuntimeException("ECG analysis failed (exit code " + exitCode + "): " + errorOutput); + } + + return objectMapper.readValue(output.toString(), ECGPredictionResponse.class); + } + + /** + * Original mode: predict from a raw signal array (180 samples). + */ + public ECGPredictionResponse predictECG(List signal) throws Exception { + + Map input = new HashMap<>(); + input.put("signal", signal); + String jsonInput = objectMapper.writeValueAsString(input); + + String absoluteScriptPath = resolveScriptPath(); + File workDir = getWorkingDirectory(); + + ProcessBuilder pb = new ProcessBuilder(pythonPath, absoluteScriptPath, "signal", jsonInput); + pb.directory(workDir); + pb.redirectErrorStream(false); + + Process process = pb.start(); + + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + String resultLine = reader.readLine(); + + BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream())); + StringBuilder errorOutput = new StringBuilder(); + String errorLine; + while ((errorLine = errorReader.readLine()) != null) { + errorOutput.append(errorLine).append("\n"); + } + + int exitCode = process.waitFor(); + + if (exitCode != 0 || resultLine == null) { + throw new RuntimeException("ECG prediction failed (exit code " + exitCode + "): " + errorOutput); + } + + return objectMapper.readValue(resultLine, ECGPredictionResponse.class); + } +} diff --git a/vitalReports-AI/src/main/resources/application.properties b/vitalReports-AI/src/main/resources/application.properties index f2ea184..ed5c4f1 100644 --- a/vitalReports-AI/src/main/resources/application.properties +++ b/vitalReports-AI/src/main/resources/application.properties @@ -6,3 +6,14 @@ eureka.client.service-url.defaultZone=${EUREKA_CLIENT_SERVICEURL_DEFAULTZONE:htt management.endpoints.web.exposure.include=health,info,prometheus management.endpoint.health.show-details=always management.metrics.tags.application=${spring.application.name} + +# ECG Arrhythmia Detection (CNN Model via Python) +# Override ECG_PYTHON_PATH env var in Docker/production (e.g. python3) +ecg.python.path=${ECG_PYTHON_PATH:python} +# Path relative to working directory - resolved by ECGPredictionService +ecg.script.path=${ECG_SCRIPT_PATH:ecg_predict.py} + +# File Upload Configuration for ECG files +spring.servlet.multipart.enabled=true +spring.servlet.multipart.max-file-size=10MB +spring.servlet.multipart.max-request-size=10MB