'datetime', 'expires_at' => 'datetime', 'reminder_sent_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'); } /** * Get the standardized iCalendar event title / name. * Pattern: Interview Invitation_{Round}_{Candidate First & Last Name}_{Job Title}_{Date}_{Time} * Example: Interview Invitation_R1_Soumyadeep Mondal_AIML (Computer Vision & Agentic AI) Engineer_11/8/2026_1700 */ public function getEventTitleAttribute(): string { $round = ! empty($this->round) ? $this->round : 'R1'; $candidateName = ! empty($this->candidate_name) ? $this->candidate_name : 'Candidate'; $jobTitle = ! empty($this->job_title) ? $this->job_title : 'Candidate Assessment'; $scheduled = $this->scheduled_at ?? $this->created_at ?? now(); $dateStr = $scheduled->format('j/n/Y'); $timeStr = $scheduled->format('Hi'); return "Interview Invitation_{$round}_{$candidateName}_{$jobTitle}_{$dateStr}_{$timeStr}"; } /** * Get the User models corresponding to the assigned interviewers. */ public function assignedUsers() { if (empty($this->assigned_interviewers)) { return collect(); } return User::whereIn('id', (array) $this->assigned_interviewers)->get(); } /** * Get the URL to view the candidate's resume. */ public function getResumeUrlAttribute(): ?string { if (! $this->resume_path) { return null; } return route('interview.resume', $this->id); } }