Created services and repositories

This commit is contained in:
2025-11-07 18:11:58 +01:00
parent 42efca48ee
commit 93858408e8
25 changed files with 954 additions and 565 deletions

View File

@@ -1,90 +1,101 @@
package com.project.back_end.DTO;
import com.project.back_end.models.Appointment;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
public class AppointmentDTO {
// 1. 'id' field:
// - Type: private Long
// - Description:
// - Represents the unique identifier for the appointment.
// - This is the primary key for identifying the appointment in the system.
// 2. 'doctorId' field:
// - Type: private Long
// - Description:
// - Represents the ID of the doctor associated with the appointment.
// - This is a simplified field, capturing only the ID of the doctor (not the full Doctor object).
private final Long id;
private final Long doctorId;
private final String doctorName;
private final Long patientId;
private final String patientName;
private final String patientEmail;
private final String patientPhone;
private final String patientAddress;
private final LocalDateTime appointmentTime;
private final LocalDate appointmentDate;
private final LocalTime appointmentTimeOnly;
private final LocalDateTime endTime;
private final int status;
// 3. 'doctorName' field:
// - Type: private String
// - Description:
// - Represents the name of the doctor associated with the appointment.
// - This is a simplified field for displaying the doctor's name.
public AppointmentDTO(Long id, Long doctorId, String doctorName, Long patientId,
String patientName, String patientEmail, String patientPhone,
String patientAddress, LocalDateTime appointmentTime, int status) {
this.id = id;
this.doctorId = doctorId;
this.doctorName = doctorName;
this.patientId = patientId;
this.patientName = patientName;
this.patientEmail = patientEmail;
this.patientPhone = patientPhone;
this.patientAddress = patientAddress;
this.appointmentTime = appointmentTime;
this.appointmentDate = appointmentTime.toLocalDate();
this.appointmentTimeOnly = appointmentTime.toLocalTime();
this.endTime = appointmentTime.plusHours(1);
this.status = status;
}
// 4. 'patientId' field:
// - Type: private Long
// - Description:
// - Represents the ID of the patient associated with the appointment.
// - This is a simplified field, capturing only the ID of the patient (not the full Patient object).
public AppointmentDTO(Appointment appointment) {
this(appointment.getId(), appointment.getDoctor().getId(), appointment.getDoctorName(),
appointment.getPatient().getId(), appointment.getPatientName(), appointment.getPatient().getEmail(),
appointment.getPatient().getPhone(), appointment.getPatient().getAddress(),
appointment.getAppointmentTime(), appointment.getStatus().ordinal());
}
// 5. 'patientName' field:
// - Type: private String
// - Description:
// - Represents the name of the patient associated with the appointment.
// - This is a simplified field for displaying the patient's name.
public Long getId() {
return id;
}
// 6. 'patientEmail' field:
// - Type: private String
// - Description:
// - Represents the email of the patient associated with the appointment.
// - This is a simplified field for displaying the patient's email.
public Long getDoctorId() {
return doctorId;
}
// 7. 'patientPhone' field:
// - Type: private String
// - Description:
// - Represents the phone number of the patient associated with the appointment.
// - This is a simplified field for displaying the patient's phone number.
public String getDoctorName() {
return doctorName;
}
// 8. 'patientAddress' field:
// - Type: private String
// - Description:
// - Represents the address of the patient associated with the appointment.
// - This is a simplified field for displaying the patient's address.
public Long getPatientId() {
return patientId;
}
// 9. 'appointmentTime' field:
// - Type: private LocalDateTime
// - Description:
// - Represents the scheduled date and time of the appointment.
// - The time when the appointment is supposed to happen, stored as a LocalDateTime object.
public String getPatientName() {
return patientName;
}
// 10. 'status' field:
// - Type: private int
// - Description:
// - Represents the status of the appointment.
// - Status can indicate if the appointment is "Scheduled:0", "Completed:1", or other statuses (e.g., "Canceled") as needed.
public String getPatientEmail() {
return patientEmail;
}
// 11. 'appointmentDate' field (Custom Getter):
// - Type: private LocalDate
// - Description:
// - A derived field representing only the date part of the appointment (without the time).
// - Extracted from the 'appointmentTime' field.
public String getPatientPhone() {
return patientPhone;
}
// 12. 'appointmentTimeOnly' field (Custom Getter):
// - Type: private LocalTime
// - Description:
// - A derived field representing only the time part of the appointment (without the date).
// - Extracted from the 'appointmentTime' field.
public String getPatientAddress() {
return patientAddress;
}
// 13. 'endTime' field (Custom Getter):
// - Type: private LocalDateTime
// - Description:
// - A derived field representing the end time of the appointment.
// - Calculated by adding 1 hour to the 'appointmentTime' field.
public LocalDateTime getAppointmentTime() {
return appointmentTime;
}
// 14. Constructor:
// - The constructor accepts all the relevant fields for the AppointmentDTO, including simplified fields for the doctor and patient (ID, name, etc.).
// - It also calculates custom fields: 'appointmentDate', 'appointmentTimeOnly', and 'endTime' based on the 'appointmentTime' field.
public LocalDate getAppointmentDate() {
return appointmentDate;
}
// 15. Getters:
// - Standard getter methods are provided for all fields: id, doctorId, doctorName, patientId, patientName, patientEmail, patientPhone, patientAddress, appointmentTime, status, appointmentDate, appointmentTimeOnly, and endTime.
// - These methods allow access to the values of the fields in the AppointmentDTO object.
public LocalTime getAppointmentTimeOnly() {
return appointmentTimeOnly;
}
}
public LocalDateTime getEndTime() {
return endTime;
}
public int getStatus() {
return status;
}
}

View File

@@ -1,30 +1,42 @@
package com.project.back_end.DTO;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class Login {
// 1. 'email' field:
// - Type: private String
// - Description:
// - Represents the email address used for logging into the system.
// - The email field is expected to contain a valid email address for user authentication purposes.
// 2. 'password' field:
// - Type: private String
// - Description:
// - Represents the password associated with the email address.
// - The password field is used for verifying the user's identity during login.
// - It is generally hashed before being stored and compared during authentication.
private String identifier;
private String password;
// 3. Constructor:
// - No explicit constructor is defined for this class, as it relies on the default constructor provided by Java.
// - This class can be initialized with setters or directly via reflection, as per the application's needs.
public Login(String identifier, String password) {
this.identifier = identifier;
this.password = hashPassword(password);
}
// 4. Getters and Setters:
// - Standard getter and setter methods are provided for both 'email' and 'password' fields.
// - The 'getEmail()' method allows access to the email value.
// - The 'setEmail(String email)' method sets the email value.
// - The 'getPassword()' method allows access to the password value.
// - The 'setPassword(String password)' method sets the password value.
public String getIdentifier() {
return identifier;
}
public String getPassword() {
return password;
}
public void setIdentifier(String identifier) {
this.identifier = identifier;
}
public void setPassword(String password) {
this.password = password;
}
private static String hashPassword(String rawPassword) {
try {
return new String(MessageDigest.getInstance("SHA-256").digest(rawPassword.getBytes(StandardCharsets.UTF_8)));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
}
}

View File

@@ -8,11 +8,14 @@ import jakarta.validation.constraints.NotNull;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
@Entity
@Table(name = "appointments", uniqueConstraints = @UniqueConstraint(name = "doctor_patient_time_constraint", columnNames = {"doctor", "patient", "appointmentTime"}))
public class Appointment {
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm");
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@@ -87,6 +90,11 @@ public class Appointment {
return appointmentTime.toLocalTime();
}
@Transient
public String getAppointmentTimeAsString() {
return TIME_FORMATTER.format(getAppointmentTimeOnly()) + "-" + TIME_FORMATTER.format(getEndTime());
}
@Transient
public String getDoctorName() {
return doctor.getName();

View File

@@ -11,6 +11,7 @@ import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Set;
@Entity
@Table(name = "doctors")
@@ -43,9 +44,14 @@ public class Doctor {
@Pattern(regexp = "^[0-9]{10}$")
private String phone;
@OneToMany(mappedBy = "doctor")
@OneToMany(mappedBy = "doctor", fetch = FetchType.LAZY)
private List<Appointment> appointments;
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "doctors_available_times", uniqueConstraints = @UniqueConstraint(columnNames = {"element_value", "doctor_id"}))
@Column(name = "available_times")
private Set<String> availableTimes;
public Doctor() {
}
@@ -107,6 +113,14 @@ public class Doctor {
this.appointments = appointments;
}
public Set<String> getAvailableTimes() {
return availableTimes;
}
public void setAvailableTimes(Set<String> availableTimes) {
this.availableTimes = availableTimes;
}
private static String hashPassword(String rawPassword) {
try {
return new String(MessageDigest.getInstance("SHA-256").digest(rawPassword.getBytes(StandardCharsets.UTF_8)));

View File

@@ -1,30 +1,32 @@
package com.project.back_end.mvc;
import com.project.back_end.services.TokenService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@Controller
public class DashboardController {
// 1. Set Up the MVC Controller Class:
// - Annotate the class with `@Controller` to indicate that it serves as an MVC controller returning view names (not JSON).
// - This class handles routing to admin and doctor dashboard pages based on token validation.
private final TokenService tokenService;
public DashboardController(TokenService tokenService) {
this.tokenService = tokenService;
}
// 2. Autowire the Shared Service:
// - Inject the common `Service` class, which provides the token validation logic used to authorize access to dashboards.
@GetMapping("/adminDashboard/{token}")
public String adminDashboard(@PathVariable("token") String token) {
if (!tokenService.validateToken(token, "admin")) {
return "redirect:/";
}
return "redirect:admin/adminDashboard";
}
// 3. Define the `adminDashboard` Method:
// - Handles HTTP GET requests to `/adminDashboard/{token}`.
// - Accepts an admin's token as a path variable.
// - Validates the token using the shared service for the `"admin"` role.
// - If the token is valid (i.e., no errors returned), forwards the user to the `"admin/adminDashboard"` view.
// - If invalid, redirects to the root URL, likely the login or home page.
// 4. Define the `doctorDashboard` Method:
// - Handles HTTP GET requests to `/doctorDashboard/{token}`.
// - Accepts a doctor's token as a path variable.
// - Validates the token using the shared service for the `"doctor"` role.
// - If the token is valid, forwards the user to the `"doctor/doctorDashboard"` view.
// - If the token is invalid, redirects to the root URL.
}
@GetMapping("/doctorDashboard/{token}")
public String doctorDashboard(@PathVariable("token") String token) {
if (!tokenService.validateToken(token, "doctor")) {
return "redirect:/";
}
return "redirect:admin/doctorDashboard";
}
}

View File

@@ -1,30 +1,14 @@
package com.project.back_end.repo;
public interface AdminRepository {
import com.project.back_end.models.Admin;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
// 1. Extend JpaRepository:
// - The repository extends JpaRepository<Admin, Long>, which gives it basic CRUD functionality.
// - The methods such as save, delete, update, and find are inherited without the need for explicit implementation.
// - JpaRepository also includes pagination and sorting features.
import java.util.Optional;
// Example: public interface AdminRepository extends JpaRepository<Admin, Long> {}
@Repository
public interface AdminRepository extends JpaRepository<Admin, Long> {
// 2. Custom Query Method:
// - **findByUsername**:
// - This method allows you to find an Admin by their username.
// - Return type: Admin
// - Parameter: String username
// - It will return an Admin entity that matches the provided username.
// - If no Admin is found with the given username, it returns null.
Optional<Admin> findByUsername(String username);
// Example: public Admin findByUsername(String username);
// 3. Add @Repository annotation:
// - The @Repository annotation marks this interface as a Spring Data JPA repository.
// - While it is technically optional (since JpaRepository is a part of Spring Data), it's good practice to include it for clarity.
// - Spring Data JPA automatically implements the repository, providing the necessary CRUD functionality.
// Example: @Repository
// public interface AdminRepository extends JpaRepository<Admin, Long> { ... }
}
}

View File

@@ -1,66 +1,68 @@
package com.project.back_end.repo;
public interface AppointmentRepository {
import com.project.back_end.models.Appointment;
import jakarta.transaction.Transactional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
// 1. Extend JpaRepository:
// - The repository extends JpaRepository<Appointment, Long>, which gives it basic CRUD functionality.
// - The methods such as save, delete, update, and find are inherited without the need for explicit implementation.
// - JpaRepository also includes pagination and sorting features.
import java.time.LocalDateTime;
import java.util.List;
// Example: public interface AppointmentRepository extends JpaRepository<Appointment, Long> {}
@Repository
public interface AppointmentRepository extends JpaRepository<Appointment, Long> {
// 2. Custom Query Methods:
@Query("""
FROM Appointment a
LEFT JOIN FETCH Doctor d ON a.doctor = d
WHERE d.id = :doctorId
AND a.appointmentTime BETWEEN :start and :end
""")
List<Appointment> findByDoctorIdAndAppointmentTimeBetween(Long doctorId, LocalDateTime start, LocalDateTime end);
// - **findByDoctorIdAndAppointmentTimeBetween**:
// - This method retrieves a list of appointments for a specific doctor within a given time range.
// - The doctors available times are eagerly fetched to avoid lazy loading.
// - Return type: List<Appointment>
// - Parameters: Long doctorId, LocalDateTime start, LocalDateTime end
// - It uses a LEFT JOIN to fetch the doctors available times along with the appointments.
@Query("""
FROM Appointment a
LEFT JOIN FETCH Doctor d ON a.doctor = d
LEFT JOIN FETCH Patient p on a.patient = p
WHERE d.id = :doctorId
AND LOWER(p.name) LIKE CONCAT('%',LOWER(:patientName),'%')
AND a.appointmentTime BETWEEN :start and :end
""")
List<Appointment> findByDoctorIdAndPatient_NameContainingIgnoreCaseAndAppointmentTimeBetween(Long doctorId, String patientName, LocalDateTime start, LocalDateTime end);
// - **findByDoctorIdAndPatient_NameContainingIgnoreCaseAndAppointmentTimeBetween**:
// - This method retrieves appointments for a specific doctor and patient name (ignoring case) within a given time range.
// - It performs a LEFT JOIN to fetch both the doctor and patient details along with the appointment times.
// - Return type: List<Appointment>
// - Parameters: Long doctorId, String patientName, LocalDateTime start, LocalDateTime end
@Transactional
@Modifying
void deleteAllByDoctorId(Long doctorId);
// - **deleteAllByDoctorId**:
// - This method deletes all appointments associated with a particular doctor.
// - It is marked as @Modifying and @Transactional, which makes it a modification query, ensuring that the operation is executed within a transaction.
// - Return type: void
// - Parameters: Long doctorId
List<Appointment> findByPatientId(Long patientId);
// - **findByPatientId**:
// - This method retrieves all appointments for a specific patient.
// - Return type: List<Appointment>
// - Parameters: Long patientId
List<Appointment> findByPatient_IdAndStatusOrderByAppointmentTimeAsc(Long patientId, int status);
// - **findByPatient_IdAndStatusOrderByAppointmentTimeAsc**:
// - This method retrieves all appointments for a specific patient with a given status, ordered by the appointment time.
// - Return type: List<Appointment>
// - Parameters: Long patientId, int status
@Query("""
FROM Appointment a
LEFT JOIN FETCH Doctor d ON a.doctor = d
LEFT JOIN FETCH Patient p on a.patient = p
WHERE LOWER(d.name) LIKE CONCAT('%', LOWER(:doctorName),'%')
AND p.id = :patientId
""")
List<Appointment> filterByDoctorNameAndPatientId(String doctorName, Long patientId);
// - **filterByDoctorNameAndPatientId**:
// - This method retrieves appointments based on a doctors name (using a LIKE query) and the patients ID.
// - Return type: List<Appointment>
// - Parameters: String doctorName, Long patientId
@Query("""
FROM Appointment a
LEFT JOIN FETCH Doctor d ON a.doctor = d
LEFT JOIN FETCH Patient p on a.patient = p
WHERE LOWER(d.name) LIKE CONCAT('%', LOWER(:doctorName),'%')
AND p.id = :patientId
AND a.status = :status
""")
List<Appointment> filterByDoctorNameAndPatientIdAndStatus(String doctorName, Long patientId, int status);
// - **filterByDoctorNameAndPatientIdAndStatus**:
// - This method retrieves appointments based on a doctors name (using a LIKE query), patients ID, and a specific appointment status.
// - Return type: List<Appointment>
// - Parameters: String doctorName, Long patientId, int status
// - **updateStatus**:
// - This method updates the status of a specific appointment based on its ID.
// - Return type: void
// - Parameters: int status, long id
// 3. @Modifying and @Transactional annotations:
// - The @Modifying annotation is used to indicate that the method performs a modification operation (like DELETE or UPDATE).
// - The @Transactional annotation ensures that the modification is done within a transaction, meaning that if any exception occurs, the changes will be rolled back.
// 4. @Repository annotation:
// - The @Repository annotation marks this interface as a Spring Data JPA repository.
// - Spring Data JPA automatically implements this repository, providing the necessary CRUD functionality and custom queries defined in the interface.
}
@Modifying
@Query("""
UPDATE Appointment a
SET a.status = :status
WHERE a.id = :id
""")
void updateStatus(int status, long id);
}

View File

@@ -1,39 +1,31 @@
package com.project.back_end.repo;
public interface DoctorRepository {
// 1. Extend JpaRepository:
// - The repository extends JpaRepository<Doctor, Long>, which gives it basic CRUD functionality.
// - This allows the repository to perform operations like save, delete, update, and find without needing to implement these methods manually.
// - JpaRepository also includes features like pagination and sorting.
import com.project.back_end.models.Doctor;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
// Example: public interface DoctorRepository extends JpaRepository<Doctor, Long> {}
import java.util.List;
import java.util.Optional;
// 2. Custom Query Methods:
@Repository
public interface DoctorRepository extends JpaRepository<Doctor, Long> {
// - **findByEmail**:
// - This method retrieves a Doctor by their email.
// - Return type: Doctor
// - Parameters: String email
Optional<Doctor> findByEmail(String email);
// - **findByNameLike**:
// - This method retrieves a list of Doctors whose name contains the provided search string (case-sensitive).
// - The `CONCAT('%', :name, '%')` is used to create a pattern for partial matching.
// - Return type: List<Doctor>
// - Parameters: String name
@Query("""
FROM Doctor d
WHERE d.name LIKE CONCAT('%', :name ,'%')
""")
List<Doctor> findByNameLike(String name);
// - **findByNameContainingIgnoreCaseAndSpecialtyIgnoreCase**:
// - This method retrieves a list of Doctors where the name contains the search string (case-insensitive) and the specialty matches exactly (case-insensitive).
// - It combines both fields for a more specific search.
// - Return type: List<Doctor>
// - Parameters: String name, String specialty
@Query("""
FROM Doctor d
WHERE LOWER(d.name) LIKE CONCAT('%', LOWER(:name), '%')
AND LOWER(d.specialty) = LOWER(:specialty)
""")
List<Doctor> findByNameContainingIgnoreCaseAndSpecialtyIgnoreCase(String name, String specialty);
// - **findBySpecialtyIgnoreCase**:
// - This method retrieves a list of Doctors with the specified specialty, ignoring case sensitivity.
// - Return type: List<Doctor>
// - Parameters: String specialty
// 3. @Repository annotation:
// - The @Repository annotation marks this interface as a Spring Data JPA repository.
// - Spring Data JPA automatically implements this repository, providing the necessary CRUD functionality and custom queries defined in the interface.
List<Doctor> findBySpecialtyIgnoreCase(String specialty);
}

View File

@@ -1,29 +1,16 @@
package com.project.back_end.repo;
public interface PatientRepository {
// 1. Extend JpaRepository:
// - The repository extends JpaRepository<Patient, Long>, which provides basic CRUD functionality.
// - This allows the repository to perform operations like save, delete, update, and find without needing to implement these methods manually.
// - JpaRepository also includes features like pagination and sorting.
import com.project.back_end.models.Patient;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
// Example: public interface PatientRepository extends JpaRepository<Patient, Long> {}
import java.util.Optional;
// 2. Custom Query Methods:
@Repository
public interface PatientRepository extends JpaRepository<Patient, Long> {
// - **findByEmail**:
// - This method retrieves a Patient by their email address.
// - Return type: Patient
// - Parameters: String email
Optional<Patient> findByEmail(String email);
// - **findByEmailOrPhone**:
// - This method retrieves a Patient by either their email or phone number, allowing flexibility for the search.
// - Return type: Patient
// - Parameters: String email, String phone
// 3. @Repository annotation:
// - The @Repository annotation marks this interface as a Spring Data JPA repository.
// - Spring Data JPA automatically implements this repository, providing the necessary CRUD functionality and custom queries defined in the interface.
}
Optional<Patient> findByEmailOrPhone(String email, String phone);
}

View File

@@ -1,21 +1,12 @@
package com.project.back_end.repo;
public interface PrescriptionRepository {
// 1. Extend MongoRepository:
// - The repository extends MongoRepository<Prescription, String>, which provides basic CRUD functionality for MongoDB.
// - This allows the repository to perform operations like save, delete, update, and find without needing to implement these methods manually.
// - MongoRepository is tailored for working with MongoDB, unlike JpaRepository which is used for relational databases.
import com.project.back_end.models.Prescription;
import org.springframework.data.mongodb.repository.MongoRepository;
// Example: public interface PrescriptionRepository extends MongoRepository<Prescription, String> {}
import java.util.List;
// 2. Custom Query Method:
public interface PrescriptionRepository extends MongoRepository<Prescription, String> {
// - **findByAppointmentId**:
// - This method retrieves a list of prescriptions associated with a specific appointment.
// - Return type: List<Prescription>
// - Parameters: Long appointmentId
// - MongoRepository automatically derives the query from the method name, in this case, it will find prescriptions by the appointment ID.
}
List<Prescription> findByAppointmentId(Long appointmentId);
}

View File

@@ -1,45 +1,101 @@
package com.project.back_end.services;
import com.project.back_end.models.Appointment;
import com.project.back_end.models.Doctor;
import com.project.back_end.repo.AppointmentRepository;
import com.project.back_end.repo.DoctorRepository;
import com.project.back_end.repo.PatientRepository;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@Service
public class AppointmentService {
// 1. **Add @Service Annotation**:
// - To indicate that this class is a service layer class for handling business logic.
// - The `@Service` annotation should be added before the class declaration to mark it as a Spring service component.
// - Instruction: Add `@Service` above the class definition.
// 2. **Constructor Injection for Dependencies**:
// - The `AppointmentService` class requires several dependencies like `AppointmentRepository`, `Service`, `TokenService`, `PatientRepository`, and `DoctorRepository`.
// - These dependencies should be injected through the constructor.
// - Instruction: Ensure constructor injection is used for proper dependency management in Spring.
private final AppointmentRepository appointmentRepository;
private final PatientRepository patientRepository;
private final DoctorRepository doctorRepository;
private final TokenService tokenService;
private final com.project.back_end.services.Service service;
// 3. **Add @Transactional Annotation for Methods that Modify Database**:
// - The methods that modify or update the database should be annotated with `@Transactional` to ensure atomicity and consistency of the operations.
// - Instruction: Add the `@Transactional` annotation above methods that interact with the database, especially those modifying data.
public AppointmentService(AppointmentRepository appointmentRepository, PatientRepository patientRepository, DoctorRepository doctorRepository, TokenService tokenService, com.project.back_end.services.Service service) {
this.appointmentRepository = appointmentRepository;
this.patientRepository = patientRepository;
this.doctorRepository = doctorRepository;
this.tokenService = tokenService;
this.service = service;
}
// 4. **Book Appointment Method**:
// - Responsible for saving the new appointment to the database.
// - If the save operation fails, it returns `0`; otherwise, it returns `1`.
// - Instruction: Ensure that the method handles any exceptions and returns an appropriate result code.
@Transactional
public int bookAppointment(@Valid Appointment appointment) {
try {
appointmentRepository.save(appointment);
return 1;
} catch (Exception e) {
return 0;
}
}
// 5. **Update Appointment Method**:
// - This method is used to update an existing appointment based on its ID.
// - It validates whether the patient ID matches, checks if the appointment is available for updating, and ensures that the doctor is available at the specified time.
// - If the update is successful, it saves the appointment; otherwise, it returns an appropriate error message.
// - Instruction: Ensure proper validation and error handling is included for appointment updates.
@Transactional
public ResponseEntity<Map<String, String>> updateAppointment(@Valid Appointment appointment) {
// 6. **Cancel Appointment Method**:
// - This method cancels an appointment by deleting it from the database.
// - It ensures the patient who owns the appointment is trying to cancel it and handles possible errors.
// - Instruction: Make sure that the method checks for the patient ID match before deleting the appointment.
if (appointmentRepository.findById(appointment.getId()).isEmpty()) {
return ResponseEntity.noContent().build();
}
// 7. **Get Appointments Method**:
// - This method retrieves a list of appointments for a specific doctor on a particular day, optionally filtered by the patient's name.
// - It uses `@Transactional` to ensure that database operations are consistent and handled in a single transaction.
// - Instruction: Ensure the correct use of transaction boundaries, especially when querying the database for appointments.
if (service.validateAppointment(appointment) < 1) {
return ResponseEntity.badRequest().build();
}
// 8. **Change Status Method**:
// - This method updates the status of an appointment by changing its value in the database.
// - It should be annotated with `@Transactional` to ensure the operation is executed in a single transaction.
// - Instruction: Add `@Transactional` before this method to ensure atomicity when updating appointment status.
try {
appointmentRepository.save(appointment);
return ResponseEntity.ok(Map.of("success", "true", "message", "Updated successfully!"));
} catch (Exception e) {
return ResponseEntity.ok(Map.of("success", "false", "message", e.getMessage()));
}
}
@Transactional
public ResponseEntity<Map<String,String>> cancelAppointment(@Valid Appointment appointment, String token) {
}
if (!tokenService.validateToken(token, "DOCTOR")) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
if (appointmentRepository.findById(appointment.getId()).isEmpty()) {
return ResponseEntity.noContent().build();
}
try {
appointmentRepository.delete(appointment);
return ResponseEntity.ok(Map.of("success", "true", "message", "Removed successfully!"));
} catch (Exception e) {
return ResponseEntity.ok(Map.of("success", "false", "message", e.getMessage()));
}
}
@Transactional
public Map<String, Object> getAppointment(String pname, LocalDate date, String token) {
String doctorEmail = tokenService.extractIdentifier(token);
Optional<Doctor> found = doctorRepository.findByEmail(doctorEmail);
if (found.isEmpty()) {
return Map.of("appointments", List.of());
}
Long doctorId = found.get().getId();
List<Appointment> appointments = appointmentRepository.findByDoctorIdAndAppointmentTimeBetween(
doctorId, date.atStartOfDay(), date.plusDays(1).atStartOfDay());
if (pname != null) {
appointments = appointments.stream().filter(a -> a.getPatientName().contains(pname)).toList();
}
return Map.of("appointments", appointments);
}
}

View File

@@ -1,92 +1,202 @@
package com.project.back_end.services;
import com.project.back_end.DTO.Login;
import com.project.back_end.models.Appointment;
import com.project.back_end.models.Doctor;
import com.project.back_end.repo.AppointmentRepository;
import com.project.back_end.repo.DoctorRepository;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@Service
public class DoctorService {
// 1. **Add @Service Annotation**:
// - This class should be annotated with `@Service` to indicate that it is a service layer class.
// - The `@Service` annotation marks this class as a Spring-managed bean for business logic.
// - Instruction: Add `@Service` above the class declaration.
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm");
// 2. **Constructor Injection for Dependencies**:
// - The `DoctorService` class depends on `DoctorRepository`, `AppointmentRepository`, and `TokenService`.
// - These dependencies should be injected via the constructor for proper dependency management.
// - Instruction: Ensure constructor injection is used for injecting dependencies into the service.
private final DoctorRepository doctorRepository;
private final AppointmentRepository appointmentRepository;
private final TokenService tokenService;
// 3. **Add @Transactional Annotation for Methods that Modify or Fetch Database Data**:
// - Methods like `getDoctorAvailability`, `getDoctors`, `findDoctorByName`, `filterDoctorsBy*` should be annotated with `@Transactional`.
// - The `@Transactional` annotation ensures that database operations are consistent and wrapped in a single transaction.
// - Instruction: Add the `@Transactional` annotation above the methods that perform database operations or queries.
public DoctorService(DoctorRepository doctorRepository, AppointmentRepository appointmentRepository, TokenService tokenService) {
this.doctorRepository = doctorRepository;
this.appointmentRepository = appointmentRepository;
this.tokenService = tokenService;
}
// 4. **getDoctorAvailability Method**:
// - Retrieves the available time slots for a specific doctor on a particular date and filters out already booked slots.
// - The method fetches all appointments for the doctor on the given date and calculates the availability by comparing against booked slots.
// - Instruction: Ensure that the time slots are properly formatted and the available slots are correctly filtered.
@Transactional
public List<String> getDoctorAvailability(Long doctorId, LocalDate date) {
Doctor doctor = doctorRepository.findById(doctorId).orElseThrow(() -> new IllegalArgumentException("Doctor not found"));
return getAvailableTimes(doctor, date);
}
// 5. **saveDoctor Method**:
// - Used to save a new doctor record in the database after checking if a doctor with the same email already exists.
// - If a doctor with the same email is found, it returns `-1` to indicate conflict; `1` for success, and `0` for internal errors.
// - Instruction: Ensure that the method correctly handles conflicts and exceptions when saving a doctor.
@Transactional
public int saveDoctor(@Valid Doctor doctor) {
if (doctorRepository.findByEmail(doctor.getEmail()).isPresent()) {
return -1;
}
try {
doctorRepository.save(doctor);
return 1;
} catch (Exception e) {
return 0;
}
}
// 6. **updateDoctor Method**:
// - Updates an existing doctor's details in the database. If the doctor doesn't exist, it returns `-1`.
// - Instruction: Make sure that the doctor exists before attempting to save the updated record and handle any errors properly.
@Transactional
public int updateDoctor(@Valid Doctor doctor) {
if (doctorRepository.findById(doctor.getId()).isEmpty()) {
return -1;
}
try {
doctorRepository.save(doctor);
return 1;
} catch (Exception e) {
return 0;
}
}
// 7. **getDoctors Method**:
// - Fetches all doctors from the database. It is marked with `@Transactional` to ensure that the collection is properly loaded.
// - Instruction: Ensure that the collection is eagerly loaded, especially if dealing with lazy-loaded relationships (e.g., available times).
@Transactional
public List<Doctor> getDoctors() {
return doctorRepository.findAll();
}
// 8. **deleteDoctor Method**:
// - Deletes a doctor from the system along with all appointments associated with that doctor.
// - It first checks if the doctor exists. If not, it returns `-1`; otherwise, it deletes the doctor and their appointments.
// - Instruction: Ensure the doctor and their appointments are deleted properly, with error handling for internal issues.
@Transactional
public int deleteDoctor(Doctor doctor) {
if (doctorRepository.findById(doctor.getId()).isEmpty()) {
return -1;
}
try {
appointmentRepository.deleteAllByDoctorId(doctor.getId());
doctorRepository.delete(doctor);
return 1;
} catch (Exception e) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return 0;
}
}
// 9. **validateDoctor Method**:
// - Validates a doctor's login by checking if the email and password match an existing doctor record.
// - It generates a token for the doctor if the login is successful, otherwise returns an error message.
// - Instruction: Make sure to handle invalid login attempts and password mismatches properly with error responses.
@Transactional
public ResponseEntity<Map<String,String>> validateDoctor(Login login) {
Optional<Doctor> found = doctorRepository.findByEmail(login.getIdentifier());
if (found.isEmpty()) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
// 10. **findDoctorByName Method**:
// - Finds doctors based on partial name matching and returns the list of doctors with their available times.
// - This method is annotated with `@Transactional` to ensure that the database query and data retrieval are properly managed within a transaction.
// - Instruction: Ensure that available times are eagerly loaded for the doctors.
Doctor doctor = found.get();
if (!doctor.getPassword().equals(login.getPassword())) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
// 11. **filterDoctorsByNameSpecilityandTime Method**:
// - Filters doctors based on their name, specialty, and availability during a specific time (AM/PM).
// - The method fetches doctors matching the name and specialty criteria, then filters them based on their availability during the specified time period.
// - Instruction: Ensure proper filtering based on both the name and specialty as well as the specified time period.
String token = tokenService.generateToken(doctor.getEmail());
return ResponseEntity.ok(Map.of("token", token));
}
// 12. **filterDoctorByTime Method**:
// - Filters a list of doctors based on whether their available times match the specified time period (AM/PM).
// - This method processes a list of doctors and their available times to return those that fit the time criteria.
// - Instruction: Ensure that the time filtering logic correctly handles both AM and PM time slots and edge cases.
@Transactional
public List<Doctor> findDoctorByName(String name) {
return doctorRepository.findByNameLike(name);
}
@Transactional
public Map<String, Object> filterDoctorsByNameSpecialityAndTime(String name, String specialty, String amOrPm) {
if (!"AM".equalsIgnoreCase(amOrPm) && !"PM".equalsIgnoreCase(amOrPm)) {
throw new IllegalArgumentException("AM/PM only accepted");
}
List<Doctor> doctors = doctorRepository.findByNameContainingIgnoreCaseAndSpecialtyIgnoreCase(name, specialty)
.stream()
.filter(d -> d.getAvailableTimes().stream().anyMatch(t -> matchesAMPM(t, amOrPm)))
.toList();
return Map.of("doctors", doctors);
}
// 13. **filterDoctorByNameAndTime Method**:
// - Filters doctors based on their name and the specified time period (AM/PM).
// - Fetches doctors based on partial name matching and filters the results to include only those available during the specified time period.
// - Instruction: Ensure that the method correctly filters doctors based on the given name and time of day (AM/PM).
@Transactional
public Map<String, Object> filterDoctorByTime(String amOrPm) {
if (!"AM".equalsIgnoreCase(amOrPm) && !"PM".equalsIgnoreCase(amOrPm)) {
throw new IllegalArgumentException("AM/PM only accepted");
}
List<Doctor> doctors = doctorRepository.findAll()
.stream()
.filter(d -> d.getAvailableTimes().stream().anyMatch(t -> matchesAMPM(t, amOrPm)))
.toList();
return Map.of("doctors", doctors);
}
// 14. **filterDoctorByNameAndSpecility Method**:
// - Filters doctors by name and specialty.
// - It ensures that the resulting list of doctors matches both the name (case-insensitive) and the specified specialty.
// - Instruction: Ensure that both name and specialty are considered when filtering doctors.
@Transactional
public Map<String, Object> filterDoctorByNameAndTime(String name, String amOrPm) {
if (!"AM".equalsIgnoreCase(amOrPm) && !"PM".equalsIgnoreCase(amOrPm)) {
throw new IllegalArgumentException("AM/PM only accepted");
}
List<Doctor> doctors = doctorRepository.findByNameLike(name)
.stream()
.filter(d -> d.getAvailableTimes().stream().anyMatch(t -> matchesAMPM(t, amOrPm)))
.toList();
return Map.of("doctors", doctors);
}
@Transactional
public Map<String, Object> filterDoctorByNameAndSpeciality(String name, String speciality) {
List<Doctor> doctors = doctorRepository.findByNameContainingIgnoreCaseAndSpecialtyIgnoreCase(name,speciality);
return Map.of("doctors", doctors);
}
// 15. **filterDoctorByTimeAndSpecility Method**:
// - Filters doctors based on their specialty and availability during a specific time period (AM/PM).
// - Fetches doctors based on the specified specialty and filters them based on their available time slots for AM/PM.
// - Instruction: Ensure the time filtering is accurately applied based on the given specialty and time period (AM/PM).
@Transactional
public Map<String, Object> filterDoctorByTimeAndSpeciality(String speciality, String amOrPm) {
if (!"AM".equalsIgnoreCase(amOrPm) && !"PM".equalsIgnoreCase(amOrPm)) {
throw new IllegalArgumentException("AM/PM only accepted");
}
List<Doctor> doctors = doctorRepository.findBySpecialtyIgnoreCase(speciality)
.stream()
.filter(d -> d.getAvailableTimes().stream().anyMatch(t -> matchesAMPM(t, amOrPm)))
.toList();
return Map.of("doctors", doctors);
}
// 16. **filterDoctorBySpecility Method**:
// - Filters doctors based on their specialty.
// - This method fetches all doctors matching the specified specialty and returns them.
// - Instruction: Make sure the filtering logic works for case-insensitive specialty matching.
@Transactional
public Map<String, Object> filterDoctorBySpeciality(String speciality) {
List<Doctor> doctors = doctorRepository.findBySpecialtyIgnoreCase(speciality);
return Map.of("doctors", doctors);
}
// 17. **filterDoctorsByTime Method**:
// - Filters all doctors based on their availability during a specific time period (AM/PM).
// - The method checks all doctors' available times and returns those available during the specified time period.
// - Instruction: Ensure proper filtering logic to handle AM/PM time periods.
@Transactional
public List<Doctor> filterDoctorsByTime(String amOrPm) {
if (!"AM".equalsIgnoreCase(amOrPm) && !"PM".equalsIgnoreCase(amOrPm)) {
throw new IllegalArgumentException("AM/PM only accepted");
}
return doctorRepository.findAll()
.stream()
.filter(d -> d.getAvailableTimes().stream().anyMatch(t -> matchesAMPM(t, amOrPm)))
.toList();
}
}
private static boolean matchesAMPM(String time, String amOrPm) {
LocalTime start = LocalTime.parse(time.substring(0, time.indexOf("-")));
return switch (amOrPm.toUpperCase()) {
case "AM" -> start.isBefore(LocalTime.NOON);
case "PM" -> start.equals(LocalTime.NOON) || start.isAfter(LocalTime.NOON);
default -> false;
};
}
private List<String> getAvailableTimes(Doctor doctor, LocalDate date) {
List<String> reservedTimes = appointmentRepository.findByDoctorIdAndAppointmentTimeBetween(doctor.getId(), date.atStartOfDay(), date.plusDays(1).atStartOfDay())
.stream()
.map(Appointment::getAppointmentTimeAsString)
.toList();
return doctor.getAvailableTimes()
.stream()
.filter(t -> !reservedTimes.contains(t))
.toList();
}
}

View File

@@ -1,58 +1,148 @@
package com.project.back_end.services;
import com.project.back_end.DTO.AppointmentDTO;
import com.project.back_end.models.Appointment;
import com.project.back_end.models.Patient;
import com.project.back_end.repo.AppointmentRepository;
import com.project.back_end.repo.PatientRepository;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@Service
public class PatientService {
// 1. **Add @Service Annotation**:
// - The `@Service` annotation is used to mark this class as a Spring service component.
// - It will be managed by Spring's container and used for business logic related to patients and appointments.
// - Instruction: Ensure that the `@Service` annotation is applied above the class declaration.
// 2. **Constructor Injection for Dependencies**:
// - The `PatientService` class has dependencies on `PatientRepository`, `AppointmentRepository`, and `TokenService`.
// - These dependencies are injected via the constructor to maintain good practices of dependency injection and testing.
// - Instruction: Ensure constructor injection is used for all the required dependencies.
private final PatientRepository patientRepository;
private final AppointmentRepository appointmentRepository;
private final TokenService tokenService;
// 3. **createPatient Method**:
// - Creates a new patient in the database. It saves the patient object using the `PatientRepository`.
// - If the patient is successfully saved, the method returns `1`; otherwise, it logs the error and returns `0`.
// - Instruction: Ensure that error handling is done properly and exceptions are caught and logged appropriately.
public PatientService(PatientRepository patientRepository, AppointmentRepository appointmentRepository, TokenService tokenService) {
this.patientRepository = patientRepository;
this.appointmentRepository = appointmentRepository;
this.tokenService = tokenService;
}
// 4. **getPatientAppointment Method**:
// - Retrieves a list of appointments for a specific patient, based on their ID.
// - The appointments are then converted into `AppointmentDTO` objects for easier consumption by the API client.
// - This method is marked as `@Transactional` to ensure database consistency during the transaction.
// - Instruction: Ensure that appointment data is properly converted into DTOs and the method handles errors gracefully.
@Transactional
public int createPatient(@Valid Patient patient) {
try {
patientRepository.save(patient);
return 1;
} catch (Exception e) {
return 0;
}
}
// 5. **filterByCondition Method**:
// - Filters appointments for a patient based on the condition (e.g., "past" or "future").
// - Retrieves appointments with a specific status (0 for future, 1 for past) for the patient.
// - Converts the appointments into `AppointmentDTO` and returns them in the response.
// - Instruction: Ensure the method correctly handles "past" and "future" conditions, and that invalid conditions are caught and returned as errors.
@Transactional
public ResponseEntity<Map<String, Object>> getPatientAppointment(Long patientId, String token) {
try {
if (!tokenService.validateToken(token, "PATIENT")) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
// 6. **filterByDoctor Method**:
// - Filters appointments for a patient based on the doctor's name.
// - It retrieves appointments where the doctors name matches the given value, and the patient ID matches the provided ID.
// - Instruction: Ensure that the method correctly filters by doctor's name and patient ID and handles any errors or invalid cases.
String patientEmail = tokenService.extractIdentifier(token);
Optional<Patient> found = patientRepository.findByEmail(patientEmail);
if (found.isEmpty()) {
return ResponseEntity.noContent().build();
}
// 7. **filterByDoctorAndCondition Method**:
// - Filters appointments based on both the doctor's name and the condition (past or future) for a specific patient.
// - This method combines filtering by doctor name and appointment status (past or future).
// - Converts the appointments into `AppointmentDTO` objects and returns them in the response.
// - Instruction: Ensure that the filter handles both doctor name and condition properly, and catches errors for invalid input.
// 8. **getPatientDetails Method**:
// - Retrieves patient details using the `tokenService` to extract the patient's email from the provided token.
// - Once the email is extracted, it fetches the corresponding patient from the `patientRepository`.
// - It returns the patient's information in the response body.
// - Instruction: Make sure that the token extraction process works correctly and patient details are fetched properly based on the extracted email.
// 9. **Handling Exceptions and Errors**:
// - The service methods handle exceptions using try-catch blocks and log any issues that occur. If an error occurs during database operations, the service responds with appropriate HTTP status codes (e.g., `500 Internal Server Error`).
// - Instruction: Ensure that error handling is consistent across the service, with proper logging and meaningful error messages returned to the client.
// 10. **Use of DTOs (Data Transfer Objects)**:
// - The service uses `AppointmentDTO` to transfer appointment-related data between layers. This ensures that sensitive or unnecessary data (e.g., password or private patient information) is not exposed in the response.
// - Instruction: Ensure that DTOs are used appropriately to limit the exposure of internal data and only send the relevant fields to the client.
Patient patient = found.get();
if (!patientId.equals(patient.getId())) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
List<AppointmentDTO> appointments = appointmentRepository.findByPatientId(patientId)
.stream()
.map(AppointmentDTO::new)
.toList();
return ResponseEntity.ok(Map.of("appointments", appointments));
} catch (Exception e) {
return ResponseEntity.internalServerError().build();
}
}
@Transactional
public ResponseEntity<Map<String, Object>> filterByCondition(String condition, Long patientId) {
if (!"PAST".equalsIgnoreCase(condition) && !"PRESENT".equalsIgnoreCase(condition)) {
return ResponseEntity.badRequest().build();
}
try {
List<AppointmentDTO> appointments = appointmentRepository.findByPatientId(patientId)
.stream()
.filter(a -> matchesCondition(a, condition))
.map(AppointmentDTO::new)
.toList();
return ResponseEntity.ok(Map.of("appointments", appointments));
} catch (Exception e) {
return ResponseEntity.internalServerError().build();
}
}
@Transactional
public ResponseEntity<Map<String, Object>> filterByDoctor(String name, Long patientId) {
try {
List<AppointmentDTO> appointments = appointmentRepository.filterByDoctorNameAndPatientId(name, patientId)
.stream()
.map(AppointmentDTO::new)
.toList();
return ResponseEntity.ok(Map.of("appointments", appointments));
} catch (Exception e) {
return ResponseEntity.internalServerError().build();
}
}
@Transactional
public ResponseEntity<Map<String, Object>> filterByDoctorAndCondition(String condition, String name, long patientId) {
if (!"PAST".equalsIgnoreCase(condition) && !"PRESENT".equalsIgnoreCase(condition)) {
return ResponseEntity.badRequest().build();
}
try {
List<AppointmentDTO> appointments = appointmentRepository.filterByDoctorNameAndPatientId(name, patientId)
.stream()
.filter(a -> matchesCondition(a, condition))
.map(AppointmentDTO::new)
.toList();
return ResponseEntity.ok(Map.of("appointments", appointments));
} catch (Exception e) {
return ResponseEntity.internalServerError().build();
}
}
@Transactional
public ResponseEntity<Map<String, Object>> getPatientDetails(String token) {
try {
if (!tokenService.validateToken(token, "PATIENT")) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
String patientEmail = tokenService.extractIdentifier(token);
Optional<Patient> found = patientRepository.findByEmail(patientEmail);
if (found.isEmpty()) {
return ResponseEntity.noContent().build();
}
Patient patient = found.get();
return ResponseEntity.ok(Map.of("patient", patient));
} catch (Exception e) {
return ResponseEntity.internalServerError().build();
}
}
private static boolean matchesCondition(Appointment appointment, String condition) {
return switch (condition.toUpperCase()) {
case "PAST" -> appointment.getAppointmentTime().isBefore(LocalDateTime.now());
case "PRESENT" -> appointment.getAppointmentTime().isAfter(LocalDateTime.now());
default -> false;
};
}
}

View File

@@ -1,34 +1,45 @@
package com.project.back_end.services;
import com.project.back_end.models.Prescription;
import com.project.back_end.repo.PrescriptionRepository;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
@Service
public class PrescriptionService {
// 1. **Add @Service Annotation**:
// - The `@Service` annotation marks this class as a Spring service component, allowing Spring's container to manage it.
// - This class contains the business logic related to managing prescriptions in the healthcare system.
// - Instruction: Ensure the `@Service` annotation is applied to mark this class as a Spring-managed service.
// 2. **Constructor Injection for Dependencies**:
// - The `PrescriptionService` class depends on the `PrescriptionRepository` to interact with the database.
// - It is injected through the constructor, ensuring proper dependency management and enabling testing.
// - Instruction: Constructor injection is a good practice, ensuring that all necessary dependencies are available at the time of service initialization.
private final PrescriptionRepository prescriptionRepository;
// 3. **savePrescription Method**:
// - This method saves a new prescription to the database.
// - Before saving, it checks if a prescription already exists for the same appointment (using the appointment ID).
// - If a prescription exists, it returns a `400 Bad Request` with a message stating the prescription already exists.
// - If no prescription exists, it saves the new prescription and returns a `201 Created` status with a success message.
// - Instruction: Handle errors by providing appropriate status codes and messages, ensuring that multiple prescriptions for the same appointment are not saved.
public PrescriptionService(PrescriptionRepository prescriptionRepository) {
this.prescriptionRepository = prescriptionRepository;
}
// 4. **getPrescription Method**:
// - Retrieves a prescription associated with a specific appointment based on the `appointmentId`.
// - If a prescription is found, it returns it within a map wrapped in a `200 OK` status.
// - If there is an error while fetching the prescription, it logs the error and returns a `500 Internal Server Error` status with an error message.
// - Instruction: Ensure that this method handles edge cases, such as no prescriptions found for the given appointment, by returning meaningful responses.
@Transactional
public ResponseEntity<Map<String, String>> savePrescription(@Valid Prescription prescription) {
try {
if (!prescriptionRepository.findByAppointmentId(prescription.getAppointment().getId()).isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("message", "Prescription already exists"));
}
prescriptionRepository.save(prescription);
return ResponseEntity.status(HttpStatus.CREATED).body(Map.of("message", "Prescription saved"));
} catch (Exception e) {
return ResponseEntity.internalServerError().body(Map.of("message", "Internal Error"));
}
}
// 5. **Exception Handling and Error Responses**:
// - Both methods (`savePrescription` and `getPrescription`) contain try-catch blocks to handle exceptions that may occur during database interaction.
// - If an error occurs, the method logs the error and returns an HTTP `500 Internal Server Error` response with a corresponding error message.
// - Instruction: Ensure that all potential exceptions are handled properly, and meaningful responses are returned to the client.
}
@Transactional
public ResponseEntity<Map<String, Object>> getPrescription(Long appointmentId) {
try {
List<Prescription> prescriptions = prescriptionRepository.findByAppointmentId(appointmentId);
return ResponseEntity.ok(Map.of("prescriptions", prescriptions));
} catch (Exception e) {
return ResponseEntity.internalServerError().body(Map.of("message", "Internal Error"));
}
}
}

View File

@@ -1,66 +1,154 @@
package com.project.back_end.services;
import com.project.back_end.DTO.Login;
import com.project.back_end.models.Admin;
import com.project.back_end.models.Appointment;
import com.project.back_end.models.Patient;
import com.project.back_end.repo.AdminRepository;
import com.project.back_end.repo.DoctorRepository;
import com.project.back_end.repo.PatientRepository;
import jakarta.transaction.Transactional;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@org.springframework.stereotype.Service
public class Service {
// 1. **@Service Annotation**
// The @Service annotation marks this class as a service component in Spring. This allows Spring to automatically detect it through component scanning
// and manage its lifecycle, enabling it to be injected into controllers or other services using @Autowired or constructor injection.
// 2. **Constructor Injection for Dependencies**
// The constructor injects all required dependencies (TokenService, Repositories, and other Services). This approach promotes loose coupling, improves testability,
// and ensures that all required dependencies are provided at object creation time.
// 3. **validateToken Method**
// This method checks if the provided JWT token is valid for a specific user. It uses the TokenService to perform the validation.
// If the token is invalid or expired, it returns a 401 Unauthorized response with an appropriate error message. This ensures security by preventing
// unauthorized access to protected resources.
// 4. **validateAdmin Method**
// This method validates the login credentials for an admin user.
// - It first searches the admin repository using the provided username.
// - If an admin is found, it checks if the password matches.
// - If the password is correct, it generates and returns a JWT token (using the admins username) with a 200 OK status.
// - If the password is incorrect, it returns a 401 Unauthorized status with an error message.
// - If no admin is found, it also returns a 401 Unauthorized.
// - If any unexpected error occurs during the process, a 500 Internal Server Error response is returned.
// This method ensures that only valid admin users can access secured parts of the system.
// 5. **filterDoctor Method**
// This method provides filtering functionality for doctors based on name, specialty, and available time slots.
// - It supports various combinations of the three filters.
// - If none of the filters are provided, it returns all available doctors.
// This flexible filtering mechanism allows the frontend or consumers of the API to search and narrow down doctors based on user criteria.
// 6. **validateAppointment Method**
// This method validates if the requested appointment time for a doctor is available.
// - It first checks if the doctor exists in the repository.
// - Then, it retrieves the list of available time slots for the doctor on the specified date.
// - It compares the requested appointment time with the start times of these slots.
// - If a match is found, it returns 1 (valid appointment time).
// - If no matching time slot is found, it returns 0 (invalid).
// - If the doctor doesnt exist, it returns -1.
// This logic prevents overlapping or invalid appointment bookings.
// 7. **validatePatient Method**
// This method checks whether a patient with the same email or phone number already exists in the system.
// - If a match is found, it returns false (indicating the patient is not valid for new registration).
// - If no match is found, it returns true.
// This helps enforce uniqueness constraints on patient records and prevent duplicate entries.
// 8. **validatePatientLogin Method**
// This method handles login validation for patient users.
// - It looks up the patient by email.
// - If found, it checks whether the provided password matches the stored one.
// - On successful validation, it generates a JWT token and returns it with a 200 OK status.
// - If the password is incorrect or the patient doesn't exist, it returns a 401 Unauthorized with a relevant error.
// - If an exception occurs, it returns a 500 Internal Server Error.
// This method ensures only legitimate patients can log in and access their data securely.
// 9. **filterPatient Method**
// This method filters a patient's appointment history based on condition and doctor name.
// - It extracts the email from the JWT token to identify the patient.
// - Depending on which filters (condition, doctor name) are provided, it delegates the filtering logic to PatientService.
// - If no filters are provided, it retrieves all appointments for the patient.
// This flexible method supports patient-specific querying and enhances user experience on the client side.
private final TokenService tokenService;
private final AdminRepository adminRepository;
private final DoctorRepository doctorRepository;
private final PatientRepository patientRepository;
private final DoctorService doctorService;
private final PatientService patientService;
}
public Service(TokenService tokenService, AdminRepository adminRepository, DoctorRepository doctorRepository,
PatientRepository patientRepository, DoctorService doctorService, PatientService patientService) {
this.tokenService = tokenService;
this.adminRepository = adminRepository;
this.doctorRepository = doctorRepository;
this.patientRepository = patientRepository;
this.doctorService = doctorService;
this.patientService = patientService;
}
@Transactional
public ResponseEntity<Map<String, String>> validateToken(String token, String user) {
if (tokenService.validateToken(token, user)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
return ResponseEntity.ok().body(Map.of("message", "User is valid"));
}
@Transactional
public ResponseEntity<Map<String, String>> validateAdmin(Admin receivedAdmin) {
try {
Optional<Admin> found = adminRepository.findByUsername(receivedAdmin.getUsername());
if (found.isEmpty()) {
return ResponseEntity.noContent().build();
}
Admin admin = found.get();
if (!receivedAdmin.getPassword().equals(admin.getPassword())) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
String token = tokenService.generateToken(receivedAdmin.getUsername());
return ResponseEntity.ok(Map.of("token", token));
} catch (Exception e) {
return ResponseEntity.internalServerError().build();
}
}
@Transactional
public Map<String, Object> filterDoctor(String name, String specialty, String time) {
if (name == null && specialty == null && time == null) {
return Map.of("doctors", doctorService.getDoctors());
} else if (name == null && specialty == null) {
return doctorService.filterDoctorByTime(time);
} else if (name == null && time == null) {
return doctorService.filterDoctorBySpeciality(specialty);
} else if (specialty == null && time == null) {
return Map.of("doctors", doctorService.findDoctorByName(name));
} else if (specialty == null) {
return doctorService.filterDoctorByNameAndTime(name, time);
} else if (time == null) {
return doctorService.filterDoctorByNameAndSpeciality(name, specialty);
} else if (name == null) {
return doctorService.filterDoctorByTimeAndSpeciality(specialty, time);
} else {
return doctorService.filterDoctorsByNameSpecialityAndTime(name, specialty, time);
}
}
@Transactional
public int validateAppointment(Appointment appointment) {
if (doctorRepository.findById(appointment.getDoctor().getId()).isEmpty()) {
return -1;
}
List<String> availableSlots = doctorService.getDoctorAvailability(appointment.getDoctor().getId(), appointment.getAppointmentDate());
if (!availableSlots.contains(appointment.getAppointmentTimeAsString())) {
return 0;
}
return 1;
}
@Transactional
public boolean validatePatient(Patient patient) {
return patientRepository.findByEmail(patient.getEmail()).isEmpty();
}
@Transactional
public ResponseEntity<Map<String, String>> validatePatientLogin(Login login) {
try {
Optional<Patient> found = patientRepository.findByEmail(login.getIdentifier());
if (found.isEmpty()) {
return ResponseEntity.noContent().build();
}
Patient patient = found.get();
if (!patient.getPassword().equalsIgnoreCase(login.getPassword())) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
String token = tokenService.generateToken(login.getIdentifier());
return ResponseEntity.ok(Map.of("token", token));
} catch (Exception e) {
return ResponseEntity.internalServerError().build();
}
}
@Transactional
public ResponseEntity<Map<String, Object>> filterPatient(String condition, String name, String token) {
try {
String patientEmail = tokenService.extractIdentifier(token);
Optional<Patient> found = patientRepository.findByEmail(patientEmail);
if (found.isEmpty()) {
return ResponseEntity.noContent().build();
}
Patient patient = found.get();
if (condition == null && name == null) {
return patientService.getPatientAppointment(patient.getId(), token);
} else if (condition == null) {
return patientService.filterByDoctor(name, patient.getId());
} else if ( name == null) {
return patientService.filterByCondition(condition, patient.getId());
} else {
return patientService.filterByDoctorAndCondition(condition, name, patient.getId());
}
} catch (Exception e) {
return ResponseEntity.internalServerError().build();
}
}
}

View File

@@ -1,43 +1,70 @@
package com.project.back_end.services;
import com.project.back_end.repo.AdminRepository;
import com.project.back_end.repo.DoctorRepository;
import com.project.back_end.repo.PatientRepository;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Date;
@Component
public class TokenService {
// 1. **@Component Annotation**
// The @Component annotation marks this class as a Spring component, meaning Spring will manage it as a bean within its application context.
// This allows the class to be injected into other Spring-managed components (like services or controllers) where it's needed.
// 2. **Constructor Injection for Dependencies**
// The constructor injects dependencies for `AdminRepository`, `DoctorRepository`, and `PatientRepository`,
// allowing the service to interact with the database and validate users based on their role (admin, doctor, or patient).
// Constructor injection ensures that the class is initialized with all required dependencies, promoting immutability and making the class testable.
private final AdminRepository adminRepository;
private final DoctorRepository doctorRepository;
private final PatientRepository patientRepository;
// 3. **getSigningKey Method**
// This method retrieves the HMAC SHA key used to sign JWT tokens.
// It uses the `jwt.secret` value, which is provided from an external source (like application properties).
// The `Keys.hmacShaKeyFor()` method converts the secret key string into a valid `SecretKey` for signing and verification of JWTs.
private final SecretKey signingKey;
private final JwtParser jwtParser;
// 4. **generateToken Method**
// This method generates a JWT token for a user based on their email.
// - The `subject` of the token is set to the user's email, which is used as an identifier.
// - The `issuedAt` is set to the current date and time.
// - The `expiration` is set to 7 days from the issue date, ensuring the token expires after one week.
// - The token is signed using the signing key generated by `getSigningKey()`, making it secure and tamper-proof.
// The method returns the JWT token as a string.
public TokenService(AdminRepository adminRepository, DoctorRepository doctorRepository, PatientRepository patientRepository, @Value("${jwt.secret}") String jwtSecret) {
this.adminRepository = adminRepository;
this.doctorRepository = doctorRepository;
this.patientRepository = patientRepository;
this.signingKey = Keys.hmacShaKeyFor(jwtSecret.getBytes());
this.jwtParser = Jwts.parser().verifyWith(signingKey).build();
}
// 5. **extractEmail Method**
// This method extracts the user's email (subject) from the provided JWT token.
// - The token is first verified using the signing key to ensure it hasnt been tampered with.
// - After verification, the token is parsed, and the subject (which represents the email) is extracted.
// This method allows the application to retrieve the user's identity (email) from the token for further use.
public String generateToken(String identifier) {
LocalDateTime now = LocalDateTime.now();
LocalDateTime exp = now.plusDays(7);
return Jwts.builder()
.subject(identifier)
.issuedAt(Date.from(now.toInstant(ZoneOffset.UTC)))
.expiration(Date.from(exp.toInstant(ZoneOffset.UTC)))
.signWith(signingKey)
.compact();
}
// 6. **validateToken Method**
// This method validates whether a provided JWT token is valid for a specific user role (admin, doctor, or patient).
// - It first extracts the email from the token using the `extractEmail()` method.
// - Depending on the role (`admin`, `doctor`, or `patient`), it checks the corresponding repository (AdminRepository, DoctorRepository, or PatientRepository)
// to see if a user with the extracted email exists.
// - If a match is found for the specified user role, it returns true, indicating the token is valid.
// - If the role or user does not exist, it returns false, indicating the token is invalid.
// - The method gracefully handles any errors by returning false if the token is invalid or an exception occurs.
// This ensures secure access control based on the user's role and their existence in the system.
public String extractIdentifier(String token) {
try {
return jwtParser.parseSignedClaims(token).getPayload().getSubject();
} catch (Exception e) {
return null;
}
}
}
public boolean validateToken(String token, String role) {
try {
String identifier = extractIdentifier(token);
if (identifier == null) {
return false;
}
return switch (role.toUpperCase()) {
case "ADMIN" -> adminRepository.findByUsername(identifier).isPresent();
case "DOCTOR" -> doctorRepository.findByEmail(identifier).isPresent();
case "PATIENT" -> patientRepository.findByEmail(identifier).isPresent();
default -> false;
};
} catch (Exception e) {
return false;
}
}
}

View File

@@ -24,9 +24,7 @@ h2 {
}
.main-content {
flex-grow: 1;
padding: 40px;
display: flex;
text-align: center;
background-image: url("index.png");
background-size: cover;

View File

@@ -17,9 +17,9 @@
<div id="header"></div>
<main class="main-content">
<h2>Select Your Role:</h2>
<button>Admin</button>
<button>Doctor</button>
<button>Patient</button>
<button id="adminLogin">Admin</button>
<button id="doctorLogin">Doctor</button>
<button id="patientLogin" onclick="window.location.href='/pages/loggedPatientDashboard.html'">Patient</button>
</main>
<div id="footer"></div>
</div>

View File

@@ -1,6 +1,6 @@
import openModal from "../components/modals.js";
import { openModal } from "../components/modals.js";
import { getDoctors, filterDoctors, saveDoctor } from "./services/doctorServices.js";
import createDoctorCard from "./components/doctorCard.js";
import { createDoctorCard } from "./components/doctorCard.js";
window.onload = function () {
document.getElementById("searchBar").addEventListener("input", filterDoctorsOnChange);

View File

@@ -1,6 +1,6 @@
import deleteDoctor from "../services/doctorService.js";
import getPatientData from "../services/patientServices.js";
import showBookingOverlay from "../loggedPatient.js";
import { deleteDoctor } from "../services/doctorServices.js";
import { getPatientData } from "../services/patientServices.js";
import { showBookingOverlay } from "../loggedPatient.js";
export function createDoctorCard(doctor) {
const card = document.createElement("div");

View File

@@ -1,4 +1,4 @@
export function renderFooter() {
function renderFooter() {
const footer = document.getElementById("footer");
footer.innerHTML = `
<footer class="footer">

View File

@@ -1,4 +1,4 @@
export function renderHeader() {
function renderHeader() {
const headerDiv = document.getElementById("header");
if (window.location.pathname.endsWith("/")) {
@@ -53,26 +53,26 @@ export function renderHeader() {
attachHeaderButtonListeners();
}
export function attachHeaderButtonListeners() {
const login = document.getElementById(patientLogin);
function attachHeaderButtonListeners() {
const login = document.getElementById("patientLogin");
if (login) {
login.addEventListener('click', () => { openModal('patientLogin'); });
}
const signup = document.getElementById(patientSignup);
const signup = document.getElementById("patientSignup");
if (signup) {
signup.addEventListener('click', () => { openModal('patientSignup'); });
}
}
export function logout() {
function logout() {
localStorage.removeItem("userRole");
localStorage.removeItem("TOKEN");
window.location.href = "/";
}
export function logoutPatient() {
function logoutPatient() {
localStorage.removeItem("userRole");
selectRole('patient')
window.location.href='/pages/loggedPatientDashboard.html
selectRole('patient');
window.location.href='/pages/loggedPatientDashboard.html';
}

View File

@@ -1,5 +1,5 @@
import getAllAppointments from "./services/appointmentRecordService.js";
import createPatientRow from "./components/patientRows.js";
import { getAllAppointments } from "./services/appointmentRecordService.js";
import { createPatientRow } from "./components/patientRows.js";
const patientTable = document.getElementById("patientTableBody");
const token = localStorage.getItem("TOKEN");

View File

@@ -1,4 +1,4 @@
import openModal from "../components/modals.js";
import { openModal } from "../components/modals.js";
import { API_BASE_URL } from "../config/config.js"
const ADMIN_API = API_BASE_URL + '/admin';
@@ -21,37 +21,43 @@ window.onload = function () {
}
}
export async function adminLoginHandler() {
window.adminLoginHandler = async function() {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const admin = { username, password };
try {
await fetch(ADMIN_API, {
const response = await fetch(ADMIN_API, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(admin)
})
.then(response => response.json())
.then(json => localStorage.setItem("TOKEN", json))
.then(item -> selectRole('admin'))
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.message);
}
localStorage.setItem("TOKEN", result)
selectRole('admin');
} catch (error) {
alert("Invalid credentials!");
}
}
export async function doctorLoginHandler() {
window.doctorLoginHandler = async function() {
const email = document.getElementById('email').value;
const password = document.getElementById('password').value;
const doctor = { email, password };
try {
await fetch(DOCTOR_API, {
const response = await fetch(DOCTOR_API, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(doctor)
})
.then(response => response.json())
.then(json => localStorage.setItem("TOKEN", json))
.then(item -> selectRole('doctor'))
const result = await response.json();
if (!response.ok) {
throw new Error(result.message);
}
localStorage.setItem("TOKEN", result)
selectRole('doctor');
} catch (error) {
alert("Invalid credentials!");
}

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="UTF-8">
<link rel="icon" type="image/svg+xml" href="../assets/images/logo/logo.png" />
<title>PatientDashboard</title>
<title>Patient Dashboard</title>
<link rel="stylesheet" href="../assets/css/adminDashboard.css">
<link rel="stylesheet" href="../assets/css/style.css">
<link rel="stylesheet" href="../assets/css/patientDashboard.css">
@@ -60,4 +60,4 @@
<script type="module" src="../js/patientDashboard.js" defer></script>
</body>
</html>
</html>