sls/app/Models/Interview.php
kushal.saha 1541c8193e feat(interview): add candidate resume upload, lightbox viewer, and panelist email attachments
- Add resume file upload to schedule drawer with instant preview and validation
- Create resume lightbox modal viewer with background scroll lock and CV SVG icon
- Integrate CV tab into inspector panel with enlarge action and state 
- Attach candidate resume to panelist notification emails when 
- Fix Outlook RFC 5546 iTIP element mismatch and add table credentials copy button
2026-08-27 11:37:01 +00:00

208 lines
6.2 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',
'job_title',
'round',
'description',
'resume_path',
'temp_password',
'scheduled_at',
'expires_at',
'reminder_sent_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',
'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);
}
}