Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.hospital.hms.medical.controller;

import com.hospital.hms.base.api.ApiResponse;
import com.hospital.hms.base.response.PaginatedResponse;
import com.hospital.hms.medical.request.SearchMedicalRecordRequest;
import com.hospital.hms.medical.response.MedicalRecordResponse;
import com.hospital.hms.medical.service.GetMedicalRecordService;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/medical-records")
@RequiredArgsConstructor
@Tag(name = "Medical Records Management", description = "Endpoints for managing medical records")
@SecurityRequirement(name = "bearerAuth")
public class MedicalRecordController {

private final GetMedicalRecordService getMedicalRecordService;

@GetMapping
@PreAuthorize("hasAnyRole('ADMIN', 'RECEPTIONIST', 'DOCTOR')")
public ResponseEntity<ApiResponse<PaginatedResponse<MedicalRecordResponse>>> getMedicalRecords(
@Valid @ModelAttribute SearchMedicalRecordRequest request
) {
PaginatedResponse<MedicalRecordResponse> data = getMedicalRecordService.execute(request);
return ResponseEntity.status(HttpStatus.OK).body(
ApiResponse.success(data, "Get medical records successfully", HttpStatus.OK.value())
);
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package com.hospital.hms.medical.repository;

import com.hospital.hms.medical.entity.MedicalRecord;
import com.hospital.hms.medical.response.MedicalRecordResponse;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;

Expand All @@ -13,4 +19,41 @@ public interface MedicalRecordRepository extends JpaRepository<MedicalRecord, UU
List<MedicalRecord> findByPatient_Id(UUID patientId);

List<MedicalRecord> findByDoctor_Id(UUID doctorId);

@Query(
value = "SELECT BIN_TO_UUID(mr.id) as id, " +
"mr.doctor_advice as doctor_advice, " +
"pa.full_name as patient_name, " +
"BIN_TO_UUID(p.id) as patient_id, da.full_name as doctor_name, " +
"BIN_TO_UUID(da.id) as doctor_id, " +
"mr.description as description, " +
"mr.created_at as created_at " +
"FROM medical_record mr " +
"JOIN patient_info p ON p.id = mr.patient_id \n" +
"JOIN account pa ON pa.id = p.account_id \n" +
"JOIN account da ON da.id = mr.doctor_id \n" +
"WHERE (:keyword IS NULL OR MATCH(pa.full_name, pa.phone) AGAINST (:keyword IN BOOLEAN MODE))\n" +
"AND (:doctorUserName IS NULL OR da.username = :doctorUserName) \n" +
"AND (:doctorName IS NULL OR LOWER(da.full_name) LIKE LOWER(CONCAT('%', :doctorName, '%')))\n" +
"AND (:from IS NULL OR mr.created_at >= :from)\n" +
"AND (:to IS NULL OR mr.created_at < :to)",
countQuery = "SELECT COUNT(*) FROM medical_record mr " +
"JOIN patient_info p ON p.id = mr.patient_id \n" +
"JOIN account pa ON pa.id = p.account_id \n" +
"JOIN account da ON da.id = mr.doctor_id \n" +
"WHERE (:keyword IS NULL OR MATCH(pa.full_name, pa.phone) AGAINST (:keyword IN BOOLEAN MODE))\n" +
"AND (:doctorUserName IS NULL OR da.username = :doctorUserName) \n" +
"AND (:doctorName IS NULL OR LOWER(da.full_name) LIKE LOWER(CONCAT('%', :doctorName, '%')))\n" +
"AND (:from IS NULL OR mr.created_at >= :from)\n" +
"AND (:to IS NULL OR mr.created_at < :to)",
nativeQuery = true
)
Page<MedicalRecordResponse> getMedicalRecordBy(
@Param("keyword") String keyword,
@Param("doctorName") String doctorName,
@Param("from") LocalDateTime from,
@Param("to") LocalDateTime to,
@Param("doctorUserName") String username,
Pageable pageable
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.hospital.hms.medical.request;

import com.hospital.hms.base.request.PaginatedRequest;
import com.hospital.hms.exception.ValidationException;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.SuperBuilder;
import org.springframework.format.annotation.DateTimeFormat;

import java.time.LocalDate;

@Getter
@Setter
@NoArgsConstructor
@SuperBuilder
@EqualsAndHashCode(callSuper = true)
//@Schema(description = "Search field for medical record")
public class SearchMedicalRecordRequest extends PaginatedRequest {

private String keyword;

private String doctorName;

@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
private LocalDate from;

@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
private LocalDate to;

@Override
public void validate() {
if ((from == null) != (to == null)) {
throw new ValidationException("Invalid date range");
}

if (from != null && to != null && from.isAfter(to)) {
throw new ValidationException("Invalid date range");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.hospital.hms.medical.response;

import java.time.LocalDateTime;
import java.util.UUID;

public interface MedicalRecordResponse {

UUID getId();

UUID getPatientId();

String getPatientName();

String getDoctorName();

UUID getDoctorId();

String getDoctorAdvice();

String getDescription();

LocalDateTime getCreatedAt();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.hospital.hms.medical.service;

import com.hospital.hms.base.response.PaginatedResponse;
import com.hospital.hms.base.service.BaseService;
import com.hospital.hms.medical.repository.MedicalRecordRepository;
import com.hospital.hms.medical.request.SearchMedicalRecordRequest;
import com.hospital.hms.medical.response.MedicalRecordResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class GetMedicalRecordService extends BaseService<SearchMedicalRecordRequest, PaginatedResponse<MedicalRecordResponse>> {

private final MedicalRecordRepository medicalRecordRepository;

@Override
@Transactional(readOnly = true)
public PaginatedResponse<MedicalRecordResponse> execute(SearchMedicalRecordRequest request) {
return super.execute(request);
}

@Override
protected PaginatedResponse<MedicalRecordResponse> doProcess(SearchMedicalRecordRequest request) {
Pageable pageable = request.toPageable();

String username = request.getUserContext() != null && request.getUserContext().hasRole("ROLE_DOCTOR") ? request.getUserContext().getUsername() : null;

Page<MedicalRecordResponse> responses = medicalRecordRepository.getMedicalRecordBy(
request.getKeyword() != null && request.getKeyword().isBlank() ? null : request.getKeyword(),
request.getDoctorName(),
request.getFrom() != null ? request.getFrom().atStartOfDay() : null,
request.getTo() != null ? request.getTo().plusDays(1).atStartOfDay() : null,
username,
pageable
);

return PaginatedResponse.from(responses);
}
}
1 change: 1 addition & 0 deletions hms-api/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ spring:
format_sql: true
default_batch_fetch_size: 16
jdbc:
time_zone: UTC
batch_size: 25
order_inserts: true
order_updates: true
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- Add full-text index on patient name and phone for medical record search
ALTER TABLE account ADD FULLTEXT INDEX ft_account_search (full_name, phone);

-- Add full-text index on medicine table for medicine search (you'll use this in Phase 5)
ALTER TABLE medicine ADD FULLTEXT INDEX ft_medicine_search (name, description);
91 changes: 91 additions & 0 deletions hms-api/src/main/resources/db/migration/V9__seed_dummy_data.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
-- =============================================================
-- V9__seed_dummy_data.sql
-- Seed employee_info, patient_info, and medical_record.
-- Accounts already exist via API registration.
-- =============================================================

-- -----------------------------------------------
-- Resolve account IDs by username
-- -----------------------------------------------
SET @acc_admin = (SELECT id FROM `account` WHERE username = 'admin.seed');
SET @acc_doctor1 = (SELECT id FROM `account` WHERE username = 'dr.nguyen');
SET @acc_doctor2 = (SELECT id FROM `account` WHERE username = 'dr.tran');
SET @acc_receptionist = (SELECT id FROM `account` WHERE username = 'receptionist.le');
SET @acc_pharmacist = (SELECT id FROM `account` WHERE username = 'pharmacist.pham');
SET @acc_cashier = (SELECT id FROM `account` WHERE username = 'cashier.hoang');
SET @acc_patient1 = (SELECT id FROM `account` WHERE username = 'patient.bui');
SET @acc_patient2 = (SELECT id FROM `account` WHERE username = 'patient.vo');
SET @acc_patient3 = (SELECT id FROM `account` WHERE username = 'patient.dang');

-- -----------------------------------------------
-- Resolve department IDs by name (set by V6)
-- -----------------------------------------------
SET @dept_internal = (SELECT id FROM `department` WHERE name = 'Internal Medicine');
SET @dept_surgery = (SELECT id FROM `department` WHERE name = 'Surgery');
SET @dept_pediatrics = (SELECT id FROM `department` WHERE name = 'Pediatrics');
SET @dept_emergency = (SELECT id FROM `department` WHERE name = 'Emergency');

-- -----------------------------------------------
-- Employee info
-- -----------------------------------------------
INSERT INTO `employee_info` (`id`, `created_at`, `updated_at`, `code`, `hire_date`, `account_id`, `department_id`)
VALUES
(UUID_TO_BIN(UUID()), NOW(), NOW(), 'EMP-ADMIN-001', '2020-01-01', @acc_admin, @dept_emergency),
(UUID_TO_BIN(UUID()), NOW(), NOW(), 'EMP-DOC-001', '2018-06-01', @acc_doctor1, @dept_internal),
(UUID_TO_BIN(UUID()), NOW(), NOW(), 'EMP-DOC-002', '2016-09-15', @acc_doctor2, @dept_surgery),
(UUID_TO_BIN(UUID()), NOW(), NOW(), 'EMP-REC-001', '2022-03-10', @acc_receptionist, @dept_emergency),
(UUID_TO_BIN(UUID()), NOW(), NOW(), 'EMP-PHA-001', '2021-07-20', @acc_pharmacist, @dept_internal),
(UUID_TO_BIN(UUID()), NOW(), NOW(), 'EMP-CAS-001', '2023-01-05', @acc_cashier, @dept_pediatrics);

-- -----------------------------------------------
-- Patient info (already created by signup flow)
-- Just look up existing IDs by account
-- -----------------------------------------------
SET @patient_info1 = (SELECT id FROM `patient_info` WHERE account_id = @acc_patient1);
SET @patient_info2 = (SELECT id FROM `patient_info` WHERE account_id = @acc_patient2);
SET @patient_info3 = (SELECT id FROM `patient_info` WHERE account_id = @acc_patient3);

-- -----------------------------------------------
-- Medical records
-- -----------------------------------------------
INSERT INTO `medical_record` (`id`, `created_at`, `updated_at`, `description`, `doctor_advice`, `doctor_id`, `patient_id`)
VALUES
(UUID_TO_BIN(UUID()), '2025-11-01 09:00:00', '2025-11-01 09:00:00',
'Patient presents with persistent cough and mild fever.',
'Rest for 3 days. Take prescribed antibiotics. Return if fever exceeds 39C.',
@acc_doctor1, @patient_info1),

(UUID_TO_BIN(UUID()), '2025-11-15 10:30:00', '2025-11-15 10:30:00',
'Routine checkup. Blood pressure slightly elevated at 140/90.',
'Reduce salt intake. Monitor blood pressure daily. Follow up in 2 weeks.',
@acc_doctor1, @patient_info2),

(UUID_TO_BIN(UUID()), '2025-12-03 08:15:00', '2025-12-03 08:15:00',
'Patient reports sharp abdominal pain, nausea for 2 days.',
'Ultrasound ordered. Avoid fatty foods. NPO until further notice.',
@acc_doctor2, @patient_info1),

(UUID_TO_BIN(UUID()), '2025-12-20 14:00:00', '2025-12-20 14:00:00',
'Post-surgery follow-up. Wound healing well, no signs of infection.',
'Continue wound dressing daily. Avoid strenuous activity for 4 weeks.',
@acc_doctor2, @patient_info3),

(UUID_TO_BIN(UUID()), '2026-01-08 11:00:00', '2026-01-08 11:00:00',
'Allergic reaction — widespread hives after taking new medication.',
'Discontinue current medication immediately. Prescribe antihistamine.',
@acc_doctor1, @patient_info3),

(UUID_TO_BIN(UUID()), '2026-01-25 09:45:00', '2026-01-25 09:45:00',
'Chronic lower back pain. MRI shows mild disc herniation at L4-L5.',
'Physical therapy 3x per week. Avoid heavy lifting. Pain relief as needed.',
@acc_doctor2, @patient_info2),

(UUID_TO_BIN(UUID()), '2026-02-10 16:30:00', '2026-02-10 16:30:00',
'Annual health screening. All vitals within normal range.',
'Maintain current lifestyle. Repeat screening in 12 months.',
@acc_doctor1, @patient_info2),

(UUID_TO_BIN(UUID()), '2026-03-05 13:00:00', '2026-03-05 13:00:00',
'Patient with type 2 diabetes, HbA1c at 8.2% — above target.',
'Adjust insulin dosage. Strict low-carb diet. Weekly glucose monitoring.',
@acc_doctor2, @patient_info1);
Loading