feat: add student conflict detection in appointments

- Add hasStudentConflict() to Appointment model
- Check student conflicts in store() and update()
- Returns 409 Conflict when student has overlapping appointment
- Fixed PostgreSQL sequence for appointments id
This commit is contained in:
Hermes Agent
2026-05-20 17:45:14 +02:00
parent 09759b7619
commit af2b179a49
2 changed files with 57 additions and 1 deletions

View File

@@ -42,6 +42,19 @@ final class AppointmentsController
}
}
// Conflict check for student
if (isset($data['student_id'], $data['start_at'], $data['end_at'])) {
if (Appointment::hasStudentConflict(
$data['student_id'] ? (int) $data['student_id'] : null,
$data['start_at'],
$data['end_at']
)) {
return $this->json($response, [
'message' => 'Student has a conflicting appointment in this time range'
], 409);
}
}
$appointment = Appointment::create([
'tenant_id' => $tenantId,
'student_id' => $data['student_id'] ?? null,
@@ -84,6 +97,20 @@ final class AppointmentsController
}
}
// Conflict check for student (exclude current appointment)
if (isset($data['student_id'], $data['start_at'], $data['end_at'])) {
if (Appointment::hasStudentConflict(
$data['student_id'] ? (int) $data['student_id'] : null,
$data['start_at'],
$data['end_at'],
(int) $args['id']
)) {
return $this->json($response, [
'message' => 'Student has a conflicting appointment in this time range'
], 409);
}
}
$data['updated_by'] = $user['id'];
$appointment->update(array_intersect_key($data, array_flip([
'student_id', 'instructor_id', 'lesson_type_id', 'title', 'start_at', 'end_at', 'units', 'status', 'notes', 'warning_acknowledged', 'updated_by'

View File

@@ -28,7 +28,6 @@ final class Appointment extends Model
/**
* Check if instructor has a conflicting appointment in the given time range.
* Excludes the appointment with $excludeId (for updates).
*/
public static function hasInstructorConflict(
int $instructorId,
@@ -52,4 +51,34 @@ final class Appointment extends Model
return $query->exists();
}
/**
* Check if student has a conflicting appointment in the given time range.
*/
public static function hasStudentConflict(
?int $studentId,
string $start,
string $end,
?int $excludeId = null
): bool {
if ($studentId === null) {
return false; // No student assigned, no conflict possible
}
$query = self::where('student_id', $studentId)
->where('status', '!=', 'cancelled')
->where(function ($q) use ($start, $end) {
$q->whereBetween('start_at', [$start, $end])
->orWhereBetween('end_at', [$start, $end])
->orWhere(function ($q2) use ($start, $end) {
$q2->where('start_at', '<=', $start)->where('end_at', '>=', $end);
});
});
if ($excludeId !== null) {
$query->where('id', '!=', $excludeId);
}
return $query->exists();
}
}