diff --git a/api/src/Controllers/AppointmentsController.php b/api/src/Controllers/AppointmentsController.php index 8c07065..e4253e5 100644 --- a/api/src/Controllers/AppointmentsController.php +++ b/api/src/Controllers/AppointmentsController.php @@ -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' diff --git a/api/src/Models/Appointment.php b/api/src/Models/Appointment.php index da59ae8..c1aae96 100644 --- a/api/src/Models/Appointment.php +++ b/api/src/Models/Appointment.php @@ -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(); + } } \ No newline at end of file