Skip to content
Open
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
Expand Up @@ -45,7 +45,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.requestMatchers("/api/appointments/availability").permitAll()
// All other appointment endpoints require authentication
.requestMatchers("/api/appointments/**").authenticated()
.requestMatchers("/api/projects/**").permitAll()
.requestMatchers("/api/projects/**").authenticated()
.requestMatchers("/api/password/forgot", "/api/password/verify-otp", "/api/password/reset").permitAll()
.requestMatchers("/api/password/change").authenticated()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
import com.example.ead_backend.service.AppointmentService;

import java.util.List;
import java.util.Map;
import java.security.Principal;
import java.time.LocalDate;

@RestController
@Slf4j
Expand Down Expand Up @@ -59,6 +61,17 @@ public List<AppointmentDTO> getAll(@RequestParam(required = false) String custom
return appointmentService.getAllAppointments();
}

// Availability: return object with 'booked' (HH:mm list) for a given date (YYYY-MM-DD)
@GetMapping("/availability")
public Map<String, Object> getAvailability(@RequestParam("date") String dateStr) {
LocalDate date = LocalDate.parse(dateStr);
List<String> booked = appointmentService.getBookedStartTimes(date);
return Map.of(
"date", dateStr,
"booked", booked
);
}

@PutMapping("/{id}")
public AppointmentDTO update(@PathVariable String id, @RequestBody AppointmentDTO dto, Principal principal) {
// Verify user owns the appointment they're trying to update
Expand Down
18 changes: 17 additions & 1 deletion src/main/java/com/example/ead_backend/model/entity/TimeLog.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,27 @@

import jakarta.persistence.*;
import lombok.Data;
import org.hibernate.annotations.UuidGenerator;
import java.time.LocalDate;

@Entity
@Table(name = "TimeLog")
@Table(name = "TimeLogs")
@Data
public class TimeLog {
@Id
@UuidGenerator
@Column(columnDefinition = "VARCHAR(255)")
private String timeLogId;

@Column(nullable = false)
private LocalDate date;

@Column(name = "start_time", nullable = false)
private String startTime; // HH:mm

@Column(name = "end_time", nullable = false)
private String endTime; // HH:mm

@Column(nullable = false)
private String type; // e.g., BLOCKED, MAINTENANCE
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
import com.example.ead_backend.model.entity.TimeLog;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.time.LocalDate;
import java.util.List;


@Repository
public interface TimeLogRepository extends JpaRepository<TimeLog, String> {
List<TimeLog> findByDate(LocalDate date);

List<TimeLog> findByDateAndType(LocalDate date, String type);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,39 @@
import com.example.ead_backend.service.AppointmentService;
import com.example.ead_backend.dto.AppointmentDTO;
import com.example.ead_backend.model.entity.Appointment;
import com.example.ead_backend.model.entity.TimeLog;
import com.example.ead_backend.model.enums.AppointmentStatus;
import com.example.ead_backend.repository.AppointmentRepository;
import com.example.ead_backend.repository.TimeLogRepository;
import com.example.ead_backend.mapper.AppointmentMapper;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.time.LocalDate;
import java.time.LocalTime;

@Service
@RequiredArgsConstructor
public class AppointmentServiceImpl implements AppointmentService {

private final AppointmentRepository appointmentRepository;
private final TimeLogRepository timeLogRepository;
private final AppointmentMapper appointmentMapper;

@Override
public AppointmentDTO createAppointment(AppointmentDTO dto) {
// Enforce unique slot per date (by start time). This is global; can be extended per-employee later.
LocalDate date = dto.getDate();
String start = dto.getStartTime();
if (date != null && start != null && appointmentRepository.existsByDateAndStartTime(date, start)) {
throw new IllegalStateException("Time slot already booked for this date");
}
Appointment entity = appointmentMapper.toEntity(dto);
// ensure new fields are copied (mapper should handle this if configured)
entity.setCustomerId(dto.getCustomerId());
Expand Down Expand Up @@ -57,6 +72,18 @@ public AppointmentDTO updateAppointment(String id, AppointmentDTO dto) {
Appointment existing = appointmentRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Appointment not found with id " + id));

// If changing date/startTime, enforce uniqueness
LocalDate newDate = dto.getDate();
String newStart = dto.getStartTime();
if (newDate != null && newStart != null) {
boolean slotTaken = appointmentRepository.existsByDateAndStartTime(newDate, newStart);
// allow updating to same slot the record already has
boolean sameSlot = newDate.equals(existing.getDate()) && newStart.equals(existing.getStartTime());
if (slotTaken && !sameSlot) {
throw new IllegalStateException("Time slot already booked for this date");
}
}

existing.setService(dto.getService());
existing.setCustomerId(dto.getCustomerId());
existing.setVehicleId(dto.getVehicleId());
Expand All @@ -79,4 +106,56 @@ public AppointmentDTO updateAppointment(String id, AppointmentDTO dto) {
public void deleteAppointment(String id) {
appointmentRepository.deleteById(id);
}

@Override
public List<String> getBookedStartTimes(LocalDate date) {
// 1) Appointment-based bookings (exclude CANCELLED)
List<String> bookedFromAppointments = appointmentRepository.findByDate(date)
.stream()
.filter(a -> a.getStatus() != AppointmentStatus.CANCELLED)
.map(Appointment::getStartTime)
.collect(Collectors.toList());

// 2) TimeLog-based blocked intervals expanded into 30-minute slot starts
List<TimeLog> logs = timeLogRepository.findByDate(date);
Set<String> bookedFromLogs = new HashSet<>();
for (TimeLog log : logs) {
LocalTime start = LocalTime.parse(safeHHMM(log.getStartTime()));
LocalTime end = LocalTime.parse(safeHHMM(log.getEndTime()));
for (LocalTime t = start; !t.isAfter(end.minusMinutes(30)); t = t.plusMinutes(30)) {
bookedFromLogs.add(toHHMM(t));
}
}

// 3) Merge and return unique HH:mm values
Set<String> all = new HashSet<>(bookedFromAppointments);
all.addAll(bookedFromLogs);
return new ArrayList<>(all);
}

private static String toHHMM(LocalTime t) {
return String.format("%02d:%02d", t.getHour(), t.getMinute());
}

private static String safeHHMM(String t) {
if (t == null) return "00:00";
String s = t.trim();
if (s.contains("-")) s = s.split("-")[0].trim();
if (s.matches("^\\d{1,2}:\\d{2}\\s*[AaPp][Mm]$")) {
// convert 12h to 24h
String[] parts = s.split(" ");
String[] hm = parts[0].split(":");
int h = Integer.parseInt(hm[0]);
String m = hm[1];
String mer = parts[1].toUpperCase();
if (mer.equals("PM") && h != 12) h += 12;
if (mer.equals("AM") && h == 12) h = 0;
return String.format("%02d:%s", h, m);
}
// strip seconds if any
if (s.matches("^\\d{2}:\\d{2}:\\d{2}$")) return s.substring(0,5);
// pad hour if needed
if (s.matches("^\\d{1}:\\d{2}$")) return "0" + s;
return s;
}
}