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:
@@ -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([
|
$appointment = Appointment::create([
|
||||||
'tenant_id' => $tenantId,
|
'tenant_id' => $tenantId,
|
||||||
'student_id' => $data['student_id'] ?? null,
|
'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'];
|
$data['updated_by'] = $user['id'];
|
||||||
$appointment->update(array_intersect_key($data, array_flip([
|
$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'
|
'student_id', 'instructor_id', 'lesson_type_id', 'title', 'start_at', 'end_at', 'units', 'status', 'notes', 'warning_acknowledged', 'updated_by'
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ final class Appointment extends Model
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if instructor has a conflicting appointment in the given time range.
|
* Check if instructor has a conflicting appointment in the given time range.
|
||||||
* Excludes the appointment with $excludeId (for updates).
|
|
||||||
*/
|
*/
|
||||||
public static function hasInstructorConflict(
|
public static function hasInstructorConflict(
|
||||||
int $instructorId,
|
int $instructorId,
|
||||||
@@ -52,4 +51,34 @@ final class Appointment extends Model
|
|||||||
|
|
||||||
return $query->exists();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user