sls/app/Models/Interview.php
kushal.saha 68e2d62888 fix: handle joining expired active calls
- Mark the call as ended when it has expired but was not gracefully terminated.
- Prevent users from joining calls that are already expired due to peers closing the tab or an ungraceful call termination.
2026-08-24 06:24:01 +00:00

157 lines
4.7 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Interview extends Model
{
use HasFactory;
protected $fillable = [
'candidate_name',
'candidate_email',
'candidate_phone',
'temp_password',
'scheduled_at',
'expires_at',
'language',
'status',
'assigned_interviewers',
'proctor_logs',
'submitted_code',
'submitted_language',
'code_output',
'recording_path',
'recordings',
'submission_unique_id',
'candidate_notes',
'candidate_drawing',
'blocked_ip',
'created_by',
'call_status',
'call_started_by',
'call_started_at',
'enable_tab_switch_screenshot',
'tab_switch_screenshots',
'active_peers',
];
protected $casts = [
'scheduled_at' => 'datetime',
'expires_at' => 'datetime',
'call_started_at' => 'datetime',
'enable_tab_switch_screenshot' => 'boolean',
'assigned_interviewers' => 'array',
'proctor_logs' => 'array',
'recordings' => 'array',
'tab_switch_screenshots' => 'array',
'active_peers' => 'array',
];
public function creator()
{
return $this->belongsTo(User::class, 'created_by');
}
public function callStarter()
{
return $this->belongsTo(User::class, 'call_started_by');
}
public function warnings()
{
return $this->hasMany(InterviewWarning::class, 'interview_id');
}
public function isExpired(): bool
{
return now()->greaterThan($this->expires_at);
}
/**
* Check if the interview call is active and unexpired.
*/
public function isCallActive(): bool
{
if ($this->isExpired() || in_array($this->status, ['completed', 'expired'])) {
return false;
}
return $this->call_status === 'active';
}
/**
* Check if the current time is within the interview timeline.
*/
public function isActiveTimeline(): bool
{
$now = now();
$start = $this->scheduled_at ?? $this->created_at;
if ($now->lessThan($start)) {
return false;
}
if ($now->greaterThan($this->expires_at)) {
return false;
}
return !in_array($this->status, ['completed', 'expired']);
}
/**
* Check if a specific user is assigned as an interviewer.
*/
public function isAssignedInterviewer($userId): bool
{
if (empty($this->assigned_interviewers)) {
return false;
}
$interviewers = array_map('strval', (array) $this->assigned_interviewers);
return in_array((string) $userId, $interviewers, true);
}
/**
* Get computed initials for candidate (e.g. "John Doe" -> "JD").
*/
public function getCandidateInitialsAttribute(): string
{
$parts = preg_split('/\s+/', trim($this->candidate_name ?? ''));
if (count($parts) >= 2) {
return strtoupper(substr($parts[0], 0, 1) . substr(end($parts), 0, 1));
}
$nameStr = trim($this->candidate_name ?? '');
return strtoupper(substr($nameStr, 0, 1) . (strlen($nameStr) > 1 ? substr($nameStr, -1) : ''));
}
/**
* Get proctoring alert metrics summary.
*/
public function getProctorMetricsAttribute(): array
{
$logs = $this->proctor_logs ?? [];
return [
'tab_switches' => count(array_filter($logs, fn($l) => ($l['type'] ?? '') === 'tab_switch')),
'focus_lost' => count(array_filter($logs, fn($l) => ($l['type'] ?? '') === 'focus_lost')),
'gaze_alerts' => count(array_filter($logs, fn($l) => in_array($l['type'] ?? '', ['gaze_anomaly', 'gaze_fixed_staring', 'looking_away']))),
'lower_gaze' => count(array_filter($logs, fn($l) => ($l['type'] ?? '') === 'gaze_lower_device')),
'question_repeat' => count(array_filter($logs, fn($l) => in_array($l['type'] ?? '', ['question_repeat_lower', 'question_repetition']))),
'external_ai' => count(array_filter($logs, fn($l) => ($l['type'] ?? '') === 'external_ai_detected')),
'paste_events' => count(array_filter($logs, fn($l) => ($l['type'] ?? '') === 'paste_event')),
'total_incidents' => count($logs),
];
}
/**
* Format timeline string: "M d, H:i H:i".
*/
public function getFormattedTimelineAttribute(): string
{
$start = $this->scheduled_at ?? $this->created_at;
return $start->format('M d, H:i') . ' ' . $this->expires_at->format('H:i');
}
}