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
865 changes: 0 additions & 865 deletions DB/reset_and_reseed.sql

This file was deleted.

416 changes: 416 additions & 0 deletions DB/seed_users.sql

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,7 @@ public ResponseEntity<?> approveAppointment(@PathVariable Long id, Authenticatio
}

try {
Appointment approved = appointmentService.approveAppointment(id);
return ResponseEntity.ok(approved);
return ResponseEntity.ok(appointmentService.approveAppointment(id));
} catch (RuntimeException e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
Expand All @@ -98,8 +97,7 @@ public ResponseEntity<?> rejectAppointment(@PathVariable Long id, @RequestBody(r
}

try {
Appointment rejected = appointmentService.rejectAppointment(id, reason);
return ResponseEntity.ok(rejected);
return ResponseEntity.ok(appointmentService.rejectAppointment(id, reason));
} catch (RuntimeException e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
Expand All @@ -111,6 +109,12 @@ public ResponseEntity<?> getAllAppointments() {
return ResponseEntity.ok(appointmentService.getAllAppointments());
}

@GetMapping("/pending")
@PreAuthorize("hasAuthority('ADMIN')")
public ResponseEntity<?> getPendingAppointments() {
return ResponseEntity.ok(appointmentService.getPendingAppointments());
}

@GetMapping("/{id}")
public ResponseEntity<?> getById(@PathVariable Long id, Authentication auth) {
return appointmentRepository.findById(id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ public Appointment createAppointment(AppointmentRequest request, String requeste
* ADMIN ONLY: Approves a pending appointment request.
*/
@Transactional
public Appointment approveAppointment(Long appointmentId) {
public AppointmentDTO approveAppointment(Long appointmentId) {
Appointment appointment = appointmentRepository.findById(appointmentId)
.orElseThrow(() -> new RuntimeException("404: Appointment not found"));

Expand All @@ -124,14 +124,15 @@ public Appointment approveAppointment(Long appointmentId) {
}

appointment.setStatus(com.securehealth.backend.model.AppointmentStatus.SCHEDULED);
return appointmentRepository.save(appointment);
Appointment saved = appointmentRepository.save(appointment);
return toDTO(saved);
}

/**
* ADMIN ONLY: Rejects a pending appointment request, freeing up the slot.
*/
@Transactional
public Appointment rejectAppointment(Long appointmentId, String rejectionReason) {
public AppointmentDTO rejectAppointment(Long appointmentId, String rejectionReason) {
Appointment appointment = appointmentRepository.findById(appointmentId)
.orElseThrow(() -> new RuntimeException("404: Appointment not found"));

Expand All @@ -140,25 +141,27 @@ public Appointment rejectAppointment(Long appointmentId, String rejectionReason)
}

appointment.setStatus(com.securehealth.backend.model.AppointmentStatus.REJECTED);
// Optional: If you added a 'adminNotes' column, you could save the reason here
// appointment.setDoctorNotes("Rejected by Admin: " + rejectionReason);
Appointment saved = appointmentRepository.save(appointment);
return toDTO(saved);
}

return appointmentRepository.save(appointment);
private AppointmentDTO toDTO(Appointment app) {
AppointmentDTO dto = new AppointmentDTO();
dto.setAppointmentId(app.getAppointmentId());
dto.setDoctorId(app.getDoctor().getUserId());
dto.setDoctorName(app.getDoctor().getEmail());
dto.setPatientName(app.getPatient().getFirstName() + " " + app.getPatient().getLastName());
dto.setAppointmentDate(app.getAppointmentDate());
dto.setStatus(app.getStatus());
dto.setReasonForVisit(app.getReasonForVisit());
return dto;
}

@Transactional(readOnly = true)
public List<AppointmentDTO> getPendingAppointments() {
return appointmentRepository.findByStatus(com.securehealth.backend.model.AppointmentStatus.PENDING_APPROVAL).stream().map(app -> {
AppointmentDTO dto = new AppointmentDTO();
dto.setAppointmentId(app.getAppointmentId());
dto.setDoctorId(app.getDoctor().getUserId());
dto.setDoctorName(app.getDoctor().getEmail());
dto.setPatientName(app.getPatient().getFirstName() + " " + app.getPatient().getLastName());
dto.setAppointmentDate(app.getAppointmentDate());
dto.setStatus(app.getStatus());
dto.setReasonForVisit(app.getReasonForVisit());
return dto;
}).collect(Collectors.toList());
return appointmentRepository.findByStatus(com.securehealth.backend.model.AppointmentStatus.PENDING_APPROVAL).stream()
.map(this::toDTO)
.collect(Collectors.toList());
}

@Transactional
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.securehealth.backend.controller;

import com.securehealth.backend.dto.AppointmentDTO;
import com.securehealth.backend.model.AppointmentStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
Expand Down Expand Up @@ -106,7 +107,7 @@ void getAppointmentsByPatient_ReturnsList() throws Exception {
@Test
void approveAppointment_AsAdmin_Returns200() throws Exception {
// Arrange
Appointment approved = new Appointment();
AppointmentDTO approved = new AppointmentDTO();
approved.setStatus(AppointmentStatus.SCHEDULED);
when(appointmentService.approveAppointment(10L)).thenReturn(approved);

Expand Down Expand Up @@ -138,7 +139,7 @@ void approveAppointment_AsPatient_Returns403() throws Exception {

@Test
void rejectAppointment_AsAdmin_Returns200() throws Exception {
Appointment rejected = new Appointment();
AppointmentDTO rejected = new AppointmentDTO();
rejected.setStatus(AppointmentStatus.REJECTED);
when(appointmentService.rejectAppointment(eq(10L), any())).thenReturn(rejected);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.securehealth.backend.service;

import com.securehealth.backend.dto.AppointmentDTO;
import com.securehealth.backend.dto.AppointmentRequest;
import com.securehealth.backend.model.Appointment;
import com.securehealth.backend.model.AppointmentStatus;
Expand Down Expand Up @@ -60,6 +61,8 @@ void setUp() {
pendingAppointment = new Appointment();
pendingAppointment.setAppointmentId(10L);
pendingAppointment.setStatus(AppointmentStatus.PENDING_APPROVAL);
pendingAppointment.setDoctor(doctorLogin);
pendingAppointment.setPatient(patientProfile);
}

@Test
Expand Down Expand Up @@ -109,7 +112,7 @@ void approveAppointment_ChangesStatusToScheduled() {
when(appointmentRepository.findById(anyLong())).thenReturn(Optional.of(pendingAppointment));
when(appointmentRepository.save(any(Appointment.class))).thenAnswer(i -> i.getArguments()[0]);

Appointment approved = appointmentService.approveAppointment(10L);
AppointmentDTO approved = appointmentService.approveAppointment(10L);

assertEquals(AppointmentStatus.SCHEDULED, approved.getStatus());
}
Expand Down
40 changes: 4 additions & 36 deletions frontend/app/src/components/admin/AppointmentApprovalQueue.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,50 +22,20 @@ const AppointmentApprovalQueue = () => {
setIsLoading(true);
setError(null);
try {
// Mock pending appointments for now
setMockPendingAppointments();
const data = await api.appointments.getPending();
setPendingAppointments(Array.isArray(data) ? data : []);
} catch (err) {
console.error('Failed to fetch appointments:', err);
setError('Failed to load pending appointments');
setMockPendingAppointments();
} finally {
setIsLoading(false);
}
};

const setMockPendingAppointments = () => {
setPendingAppointments([
{
appointmentId: 1,
patientName: 'John Smith',
patientId: 1,
doctorName: 'Dr. Sarah Johnson',
doctorId: 10,
appointmentDate: new Date(Date.now() + 3600000).toISOString(),
reasonForVisit: 'Regular check-up',
status: 'PENDING_APPROVAL',
requestedAt: new Date().toISOString()
},
{
appointmentId: 2,
patientName: 'Alice Brown',
patientId: 2,
doctorName: 'Dr. Michael Davis',
doctorId: 11,
appointmentDate: new Date(Date.now() + 7200000).toISOString(),
reasonForVisit: 'Follow-up consultation',
status: 'PENDING_APPROVAL',
requestedAt: new Date().toISOString()
}
]);
};

const handleApprove = async (appointmentId) => {
try {
await api.appointments.approve(appointmentId);
setPendingAppointments(prev =>
prev.filter(apt => apt.appointmentId !== appointmentId)
);
await fetchPendingAppointments();
setConfirmModal({ isOpen: false, action: null });
setSelectedAppt(null);
alert('Appointment approved successfully!');
Expand All @@ -78,9 +48,7 @@ const AppointmentApprovalQueue = () => {
const handleReject = async (appointmentId, reason) => {
try {
await api.appointments.reject(appointmentId, reason);
setPendingAppointments(prev =>
prev.filter(apt => apt.appointmentId !== appointmentId)
);
await fetchPendingAppointments();
setConfirmModal({ isOpen: false, action: null });
setSelectedAppt(null);
setRejectionReason('');
Expand Down
8 changes: 4 additions & 4 deletions frontend/app/src/pages/ForgotPassword.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ describe('ForgotPassword Page', () => {
render(<ForgotPassword />);

const emailInput = screen.getByPlaceholderText(/email/i);
await userEvent.type(emailInput, 'invalidemail');
fireEvent.change(emailInput, { target: { value: 'invalidemail' } });
fireEvent.blur(emailInput);

await waitFor(() => {
Expand All @@ -65,7 +65,7 @@ describe('ForgotPassword Page', () => {
render(<ForgotPassword />);

const emailInput = screen.getByPlaceholderText(/email/i);
await userEvent.type(emailInput, 'test@example.com');
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });

const submitButton = screen.getByRole('button', { name: /send reset link/i });
fireEvent.click(submitButton);
Expand All @@ -81,7 +81,7 @@ describe('ForgotPassword Page', () => {
render(<ForgotPassword />);

const emailInput = screen.getByPlaceholderText(/email/i);
await userEvent.type(emailInput, 'test@example.com');
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });

const submitButton = screen.getByRole('button', { name: /send reset link/i });
fireEvent.click(submitButton);
Expand Down Expand Up @@ -118,7 +118,7 @@ describe('ForgotPassword Page', () => {
render(<ForgotPassword />);

const emailInput = screen.getByPlaceholderText(/email/i);
await userEvent.type(emailInput, 'test@example.com');
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });

const submitButton = screen.getByRole('button', { name: /send reset link/i });
expect(submitButton).not.toBeDisabled();
Expand Down
10 changes: 5 additions & 5 deletions frontend/app/src/pages/patient/Appointments.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,11 @@ const PatientAppointments = () => {
const filteredAppointments = useMemo(() => {
return appointments.filter(a => {
if (activeTab === 'upcoming') {
return !['COMPLETED', 'CANCELLED'].includes(a.statusRaw || 'PENDING_APPROVAL');
return !['COMPLETED', 'CANCELLED', 'REJECTED'].includes(a.statusRaw || 'PENDING_APPROVAL');
} else if (activeTab === 'past') {
return a.statusRaw === 'COMPLETED';
} else if (activeTab === 'cancelled') {
return a.statusRaw === 'CANCELLED';
return ['CANCELLED', 'REJECTED'].includes(a.statusRaw);
}
return true;
});
Expand All @@ -131,7 +131,7 @@ const PatientAppointments = () => {
// Get next 3 upcoming appointments for reminder sidebar
const upcomingReminders = useMemo(() => {
return appointments
.filter(a => !['COMPLETED', 'CANCELLED'].includes(a.statusRaw || ''))
.filter(a => !['COMPLETED', 'CANCELLED', 'REJECTED'].includes(a.statusRaw || ''))
.sort((a, b) => new Date(a.startTime) - new Date(b.startTime))
.slice(0, 3);
}, [appointments]);
Expand Down Expand Up @@ -568,9 +568,9 @@ const PatientAppointments = () => {
<span className="ml-1.5 bg-gray-100 dark:bg-slate-700 text-gray-600 dark:text-slate-300 py-0.5 px-1.5 rounded text-xs">
{
appointments.filter(a => {
if (tab.key === 'upcoming') return !['COMPLETED', 'CANCELLED'].includes(a.statusRaw);
if (tab.key === 'upcoming') return !['COMPLETED', 'CANCELLED', 'REJECTED'].includes(a.statusRaw);
if (tab.key === 'past') return a.statusRaw === 'COMPLETED';
if (tab.key === 'cancelled') return a.statusRaw === 'CANCELLED';
if (tab.key === 'cancelled') return ['CANCELLED', 'REJECTED'].includes(a.statusRaw);
return false;
}).length
}
Expand Down
22 changes: 22 additions & 0 deletions frontend/app/src/services/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,28 @@ export const appointmentAPI = {
});
},

// Get all pending-approval appointments (ADMIN only)
getPending: async () => {
return apiCall('/appointments/pending', {
method: 'GET',
});
},

// Approve a pending appointment (ADMIN only)
approve: async (id) => {
return apiCall(`/appointments/${id}/approve`, {
method: 'PUT',
});
},

// Reject a pending appointment with optional reason (ADMIN only)
reject: async (id, reason = '') => {
return apiCall(`/appointments/${id}/reject`, {
method: 'PUT',
body: JSON.stringify(reason),
});
},

// Delete appointment
delete: async (id) => {
return apiCall(`/appointments/${id}`, {
Expand Down
Loading