diff --git a/app/Console/Commands/GenerateSitemap.php b/app/Console/Commands/GenerateSitemap.php index b94617b..cc67636 100644 --- a/app/Console/Commands/GenerateSitemap.php +++ b/app/Console/Commands/GenerateSitemap.php @@ -9,7 +9,7 @@ class GenerateSitemap extends Command { protected $signature = 'sitemap:generate'; - + protected $description = 'Generate the SEO sitemap for public pages.'; public function handle(): void @@ -30,4 +30,4 @@ public function handle(): void $this->info('Sitemap generated successfully in the public folder.'); } -} \ No newline at end of file +} diff --git a/app/Enums/ApplicationStatus.php b/app/Enums/ApplicationStatus.php index 2d67653..aeb74a3 100644 --- a/app/Enums/ApplicationStatus.php +++ b/app/Enums/ApplicationStatus.php @@ -10,7 +10,7 @@ enum ApplicationStatus: string case Offered = 'offered'; case Rejected = 'rejected'; - public function getLabel(): ?string + public function getLabel(): string { return match ($this) { self::Saved => 'Wishlist', @@ -21,7 +21,7 @@ public function getLabel(): ?string }; } - public function getColor(): string | array | null + public function getColor(): string { return match ($this) { self::Saved => 'gray', @@ -32,7 +32,7 @@ public function getColor(): string | array | null }; } - public function getIcon(): ?string + public function getIcon(): string { return match ($this) { self::Saved => 'heroicon-m-bookmark', diff --git a/app/Events/MatchReportUpdated.php b/app/Events/MatchReportUpdated.php index f79d04a..f2a58ad 100644 --- a/app/Events/MatchReportUpdated.php +++ b/app/Events/MatchReportUpdated.php @@ -13,14 +13,12 @@ class MatchReportUpdated implements ShouldBroadcast { use Dispatchable, InteractsWithSockets, SerializesModels; - public function __construct(public MatchReport $matchReport) - { - } + public function __construct(public MatchReport $matchReport) {} public function broadcastOn(): array { return [ - new Channel('match-reports.' . $this->matchReport->user_id), + new Channel('match-reports.'.$this->matchReport->user_id), ]; } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/ApplicationController.php b/app/Http/Controllers/ApplicationController.php index ca3e351..82347bb 100644 --- a/app/Http/Controllers/ApplicationController.php +++ b/app/Http/Controllers/ApplicationController.php @@ -2,8 +2,8 @@ namespace App\Http\Controllers; -use Illuminate\Http\Request; use App\Models\JobPosting; +use Illuminate\Http\Request; class ApplicationController extends Controller { @@ -41,7 +41,7 @@ public function show(JobPosting $jobPosting) { // FIX: Pass the $jobPosting model to the view as 'application' return view('applications.show', [ - 'application' => $jobPosting + 'application' => $jobPosting, ]); } @@ -75,4 +75,4 @@ public function destroy(JobPosting $jobPosting) return redirect()->route('applications.index')->with('success', 'Job posting deleted successfully.'); } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Auth/GoogleController.php b/app/Http/Controllers/Auth/GoogleController.php index f12e409..73511b7 100644 --- a/app/Http/Controllers/Auth/GoogleController.php +++ b/app/Http/Controllers/Auth/GoogleController.php @@ -5,9 +5,9 @@ use App\Http\Controllers\Controller; use App\Models\User; use Illuminate\Http\Request; -use Illuminate\Support\Str; -use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Str; use Laravel\Socialite\Facades\Socialite; class GoogleController extends Controller @@ -15,23 +15,26 @@ class GoogleController extends Controller public function login() { session(['google_auth_intent' => 'login']); + return Socialite::driver('google')->redirect(); } public function register() { session(['google_auth_intent' => 'register']); + return Socialite::driver('google')->redirect(); } public function callback(Request $request) { try { - // Using stateless() prevents the InvalidStateException caused by - // dropped cookies when routing through tunnels like Expose or Ngrok. - $googleUser = Socialite::driver('google')->stateless()->user(); + /** @var \Laravel\Socialite\Two\GoogleProvider $driver */ + $driver = Socialite::driver('google'); + $googleUser = $driver->stateless()->user(); } catch (\Exception $e) { - \Log::error('Google Auth Failed: ' . $e->getMessage()); + \Log::error('Google Auth Failed: '.$e->getMessage()); + return redirect()->route('login')->withErrors([ 'email' => 'Google authentication failed. Please try again.', ]); @@ -44,7 +47,7 @@ public function callback(Request $request) ->first(); if ($intent === 'login') { - if (!$existingUser) { + if (! $existingUser) { return redirect()->route('register')->withErrors([ 'email' => 'No account found with this Google account. Please register first.', ]); @@ -56,8 +59,8 @@ public function callback(Request $request) ]); Auth::login($existingUser, true); - $request->session()->regenerate(); // CRITICAL: Persists the session state - + $request->session()->regenerate(); + return redirect()->intended(route('dashboard')); } @@ -77,8 +80,8 @@ public function callback(Request $request) ]); Auth::login($newUser, true); - $request->session()->regenerate(); // CRITICAL: Persists the session state - + $request->session()->regenerate(); + return redirect()->intended(route('dashboard')); } diff --git a/app/Http/Controllers/ContactController.php b/app/Http/Controllers/ContactController.php index 8e42cd5..7b386e2 100644 --- a/app/Http/Controllers/ContactController.php +++ b/app/Http/Controllers/ContactController.php @@ -2,9 +2,9 @@ namespace App\Http\Controllers; +use App\Mail\ContactSupportMessage; use Illuminate\Http\Request; use Illuminate\Support\Facades\Mail; -use App\Mail\ContactSupportMessage; class ContactController extends Controller { @@ -25,4 +25,4 @@ public function submit(Request $request) return back()->with('status', 'Your message has been sent successfully.'); } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index 2b1e8fb..7eec728 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -2,9 +2,8 @@ namespace App\Http\Controllers; -use Illuminate\Http\Request; -use App\Models\Resume; use App\Models\MatchReport; +use App\Models\Resume; use Illuminate\Support\Str; class DashboardController extends Controller @@ -17,10 +16,10 @@ public function index() // Calculate total reports and average score in one database trip $reportStats = MatchReport::where('user_id', $user->id) - ->selectRaw(" + ->selectRaw(' count(*) as total_reports, avg(score) as average_score - ")->first(); + ')->first(); $totalReports = (int) ($reportStats->total_reports ?? 0); $averageScore = round((float) ($reportStats->average_score ?? 0)); @@ -35,36 +34,36 @@ public function index() $recentReports = $baseReports->take(3); $trendReports = $baseReports->reverse()->values(); - $chartLabels = $trendReports->map(fn($r) => $r->created_at->format('M d'))->toArray(); + $chartLabels = $trendReports->map(fn ($r) => $r->created_at->format('M d'))->toArray(); $chartData = $trendReports->pluck('score')->toArray(); - - $chartTooltips = $trendReports->map(fn($r) => Str::limit($r->jobPosting->title ?? 'Unknown Job', 30))->toArray(); + + $chartTooltips = $trendReports->map(fn ($r) => Str::limit($r->jobPosting->title ?? 'Unknown Job', 30))->toArray(); $readiness = 0; - $readinessMessage = ""; - $readinessSubtext = ""; + $readinessMessage = ''; + $readinessSubtext = ''; if ($totalResumes === 0 && $totalReports === 0) { $readiness = 0; - $readinessMessage = "Awaiting Data"; - $readinessSubtext = "Upload your first resume to establish your baseline."; + $readinessMessage = 'Awaiting Data'; + $readinessSubtext = 'Upload your first resume to establish your baseline.'; } elseif ($totalResumes > 0 && $totalReports === 0) { $readiness = 20; - $readinessMessage = "Baseline Established"; - $readinessSubtext = "Run your first match report to calibrate your true readiness score."; + $readinessMessage = 'Baseline Established'; + $readinessSubtext = 'Run your first match report to calibrate your true readiness score.'; } else { - $readiness = 40 + ($averageScore * 0.6); + $readiness = 40 + ($averageScore * 0.6); $readiness = min(100, round($readiness)); - + if ($readiness < 60) { - $readinessMessage = "Needs Optimization"; - $readinessSubtext = "Your tailored resumes need more alignment with the jobs you are targeting."; + $readinessMessage = 'Needs Optimization'; + $readinessSubtext = 'Your tailored resumes need more alignment with the jobs you are targeting.'; } elseif ($readiness < 85) { - $readinessMessage = "Strong Contender"; - $readinessSubtext = "You are matching well. Keep refining your keywords to reach the top tier."; + $readinessMessage = 'Strong Contender'; + $readinessSubtext = 'You are matching well. Keep refining your keywords to reach the top tier.'; } else { - $readinessMessage = "Highly Optimized"; - $readinessSubtext = "Your application materials are exceptionally aligned. You are ready to apply."; + $readinessMessage = 'Highly Optimized'; + $readinessSubtext = 'Your application materials are exceptionally aligned. You are ready to apply.'; } } @@ -81,4 +80,4 @@ public function index() 'chartTooltips' )); } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/MatchReportController.php b/app/Http/Controllers/MatchReportController.php index 66bf084..209aa90 100644 --- a/app/Http/Controllers/MatchReportController.php +++ b/app/Http/Controllers/MatchReportController.php @@ -4,9 +4,9 @@ namespace App\Http\Controllers; -use Illuminate\Http\Request; use App\Models\MatchReport; use App\Services\MatchAnalysisService; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Gate; class MatchReportController extends Controller @@ -24,7 +24,7 @@ public function create() public function store(Request $request, MatchAnalysisService $matchService) { $request->validate([ - 'resume_id' => 'required|exists:resumes,id', + 'resume_id' => 'required|exists:resumes,id', 'job_posting_id' => 'required|exists:job_postings,id', ]); @@ -69,4 +69,4 @@ public function destroy(MatchReport $matchReport) return redirect()->route('matches.index')->with('success', 'Report deleted successfully.'); } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index 7a49977..72418dd 100644 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -6,8 +6,8 @@ use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Redirect; use Illuminate\Support\Facades\RateLimiter; +use Illuminate\Support\Facades\Redirect; use Illuminate\View\View; class ProfileController extends Controller @@ -21,11 +21,12 @@ public function edit(Request $request): View public function update(ProfileUpdateRequest $request): RedirectResponse { - $limitKey = 'update-profile:' . $request->user()->id; + $limitKey = 'update-profile:'.$request->user()->id; if (RateLimiter::tooManyAttempts($limitKey, 5)) { $seconds = RateLimiter::availableIn($limitKey); - return Redirect::route('profile.edit')->with('error', 'Please wait ' . $seconds . ' seconds before trying again.'); + + return Redirect::route('profile.edit')->with('error', 'Please wait '.$seconds.' seconds before trying again.'); } RateLimiter::hit($limitKey, 60); @@ -38,7 +39,7 @@ public function update(ProfileUpdateRequest $request): RedirectResponse public function destroy(Request $request): RedirectResponse { - $limitKey = 'delete-account:' . $request->user()->id; + $limitKey = 'delete-account:'.$request->user()->id; if (RateLimiter::tooManyAttempts($limitKey, 5)) { return Redirect::route('profile.edit')->with('error', 'Too many attempts. Please try again later.'); @@ -62,4 +63,4 @@ public function destroy(Request $request): RedirectResponse return Redirect::to('/')->with('success', 'Your account has been permanently deleted.'); } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/ResumeController.php b/app/Http/Controllers/ResumeController.php index e8c4d96..2931b8e 100644 --- a/app/Http/Controllers/ResumeController.php +++ b/app/Http/Controllers/ResumeController.php @@ -4,9 +4,9 @@ namespace App\Http\Controllers; -use Illuminate\Http\Request; use App\Models\Resume; use App\Services\ResumeParserService; +use Illuminate\Http\Request; class ResumeController extends Controller { @@ -19,17 +19,17 @@ public function store(Request $request, ResumeParserService $parser) { $request->validate([ 'label' => 'required|string|max:255', - 'file' => 'required|file|mimes:pdf|max:5120', + 'file' => 'required|file|mimes:pdf|max:5120', ]); $uploadResult = $parser->processUpload($request->file('file')); Resume::create([ - 'user_id' => auth()->id(), - 'label' => $request->label, - 'file_url' => $uploadResult['file_url'], + 'user_id' => auth()->id(), + 'label' => $request->label, + 'file_url' => $uploadResult['file_url'], 'content_raw' => $uploadResult['content_raw'], - 'is_active' => true, + 'is_active' => true, ]); return redirect()->route('resumes.index')->with('success', 'Resume uploaded and parsed successfully.'); @@ -59,4 +59,4 @@ public function destroy(Resume $resume) return redirect()->route('resumes.index')->with('success', 'Resume deleted successfully.'); } -} \ No newline at end of file +} diff --git a/app/Http/Requests/Auth/LoginRequest.php b/app/Http/Requests/Auth/LoginRequest.php index 953b893..5bc4698 100644 --- a/app/Http/Requests/Auth/LoginRequest.php +++ b/app/Http/Requests/Auth/LoginRequest.php @@ -4,7 +4,6 @@ use App\Models\User; use Illuminate\Auth\Events\Lockout; -use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; @@ -35,7 +34,7 @@ public function authenticate(): void $user = User::where('email', $this->input('email'))->first(); // Intercept OAuth-provisioned accounts to prevent password fallback bypass - if ($user && !empty($user->google_id)) { + if ($user && ! empty($user->google_id)) { RateLimiter::hit($this->throttleKey()); throw ValidationException::withMessages([ @@ -44,10 +43,10 @@ public function authenticate(): void } // Seamlessly upgrade legacy Bcrypt hashes to Argon2id - if ($user && !empty($user->password) && str_starts_with($user->password, '$2y$')) { + if ($user && ! empty($user->password) && str_starts_with($user->password, '$2y$')) { if (password_verify($this->input('password'), $user->password)) { $user->update([ - 'password' => Hash::make($this->input('password')) + 'password' => Hash::make($this->input('password')), ]); } else { RateLimiter::hit($this->throttleKey()); @@ -99,4 +98,4 @@ public function throttleKey(): string { return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip()); } -} \ No newline at end of file +} diff --git a/app/Http/Requests/ProfileUpdateRequest.php b/app/Http/Requests/ProfileUpdateRequest.php index ed54d85..2b317f9 100644 --- a/app/Http/Requests/ProfileUpdateRequest.php +++ b/app/Http/Requests/ProfileUpdateRequest.php @@ -12,4 +12,4 @@ public function rules(): array 'name' => ['required', 'string', 'max:255'], ]; } -} \ No newline at end of file +} diff --git a/app/Jobs/GenerateMatchReport.php b/app/Jobs/GenerateMatchReport.php index c1d7e69..2df256f 100644 --- a/app/Jobs/GenerateMatchReport.php +++ b/app/Jobs/GenerateMatchReport.php @@ -4,11 +4,11 @@ namespace App\Jobs; +use App\Events\MatchReportUpdated; +use App\Models\JobPosting; use App\Models\MatchReport; use App\Models\Resume; -use App\Models\JobPosting; use App\Services\MatchAnalysisService; -use App\Events\MatchReportUpdated; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Queue\Queueable; use Illuminate\Support\Facades\Log; @@ -28,8 +28,9 @@ public function handle(MatchAnalysisService $matchService): void $resume = Resume::find($this->matchReport->resume_id); $jobPosting = JobPosting::find($this->matchReport->job_id); - if (!$resume || !$jobPosting) { + if (! $resume || ! $jobPosting) { $this->updateStatus('failed', 'System Error: Missing records.'); + return; } @@ -37,29 +38,29 @@ public function handle(MatchAnalysisService $matchService): void $analysis = $matchService->analyze($resume, $jobPosting); $this->matchReport->update([ - 'score' => $analysis['score'] ?? 0, + 'score' => $analysis['score'] ?? 0, 'missing_keywords' => $analysis['missing_keywords'] ?? [], - 'reasoning' => $analysis['reasoning'] ?? 'Analysis complete.', + 'reasoning' => $analysis['reasoning'] ?? 'Analysis complete.', ]); $this->updateStatus('completed'); } catch (\Throwable $e) { - Log::error('Match Report Generation Failed: ' . $e->getMessage()); + Log::error('Match Report Generation Failed: '.$e->getMessage()); $this->updateStatus('failed', 'An error occurred during analysis.'); } } - private function updateStatus(string $status, string $reasoning = null): void + private function updateStatus(string $status, ?string $reasoning = null): void { $data = ['status' => $status]; - + if ($reasoning) { $data['reasoning'] = $reasoning; } $this->matchReport->update($data); - + event(new MatchReportUpdated($this->matchReport)); } -} \ No newline at end of file +} diff --git a/app/Livewire/ActionPlanWidget.php b/app/Livewire/ActionPlanWidget.php index 7182b8f..cb5b387 100644 --- a/app/Livewire/ActionPlanWidget.php +++ b/app/Livewire/ActionPlanWidget.php @@ -2,15 +2,17 @@ namespace App\Livewire; -use Livewire\Component; use App\Models\MatchReport; use App\Services\GritActionPlanService; use Exception; +use Livewire\Component; class ActionPlanWidget extends Component { public MatchReport $matchReport; + public bool $isLoading = false; + public ?string $errorMessage = null; public function generatePlan(GritActionPlanService $service) @@ -22,7 +24,7 @@ public function generatePlan(GritActionPlanService $service) $service->generatePlan($this->matchReport); $this->matchReport->refresh(); } catch (Exception $e) { - $this->errorMessage = "We could not generate the plan right now. Please try again later."; + $this->errorMessage = 'We could not generate the plan right now. Please try again later.'; } finally { $this->isLoading = false; } @@ -32,4 +34,4 @@ public function render() { return view('livewire.action-plan-widget'); } -} \ No newline at end of file +} diff --git a/app/Livewire/ApplicationBoard.php b/app/Livewire/ApplicationBoard.php index 458e04e..5d6f5fc 100644 --- a/app/Livewire/ApplicationBoard.php +++ b/app/Livewire/ApplicationBoard.php @@ -2,11 +2,11 @@ namespace App\Livewire; -use Livewire\Component; -use App\Models\JobPosting; use App\Enums\ApplicationStatus; +use App\Models\JobPosting; use Livewire\Attributes\Layout; use Livewire\Attributes\Title; +use Livewire\Component; #[Layout('layouts.app')] #[Title('Application Board')] @@ -20,10 +20,10 @@ public function updateStatus($id, $newStatus) if ($job) { $validStatuses = array_column(ApplicationStatus::cases(), 'value'); - + if (in_array($newStatus, $validStatuses)) { $job->update(['status' => $newStatus]); - + $statusEnum = ApplicationStatus::tryFrom($newStatus); $this->dispatch('notify', message: "Moved to {$statusEnum->getLabel()}"); } @@ -36,7 +36,7 @@ public function render() $jobs = JobPosting::select('id', 'title', 'company', 'status', 'updated_at') ->where('user_id', auth()->id()) ->when($this->search, function ($query) { - $searchTerm = '%' . trim($this->search) . '%'; + $searchTerm = '%'.trim($this->search).'%'; $query->where(function ($q) use ($searchTerm) { $q->where('company', 'like', $searchTerm) ->orWhere('title', 'like', $searchTerm); @@ -47,7 +47,7 @@ public function render() return view('livewire.application-board', [ 'jobs' => $jobs, - 'statuses' => ApplicationStatus::cases() + 'statuses' => ApplicationStatus::cases(), ]); } -} \ No newline at end of file +} diff --git a/app/Livewire/ApplicationIndex.php b/app/Livewire/ApplicationIndex.php index 92788da..6338d5b 100644 --- a/app/Livewire/ApplicationIndex.php +++ b/app/Livewire/ApplicationIndex.php @@ -2,10 +2,10 @@ namespace App\Livewire; -use Livewire\Component; -use Livewire\WithPagination; use App\Models\JobPosting; use Livewire\Attributes\Url; +use Livewire\Component; +use Livewire\WithPagination; class ApplicationIndex extends Component { @@ -44,7 +44,7 @@ public function updateStatus($id, $newStatus) if ($job) { $job->update(['status' => $newStatus]); - $this->dispatch('notify', message: 'Status updated to ' . ucfirst($newStatus) . '.'); + $this->dispatch('notify', message: 'Status updated to '.ucfirst($newStatus).'.'); } } @@ -87,7 +87,7 @@ public function render() $jobs = (clone $baseQuery) ->when($this->search, function ($query) { - $searchTerm = '%' . trim($this->search) . '%'; + $searchTerm = '%'.trim($this->search).'%'; $query->where(function ($q) use ($searchTerm) { $q->where('company', 'like', $searchTerm) ->orWhere('title', 'like', $searchTerm); @@ -107,4 +107,4 @@ public function render() 'hasAnyJobs' => ($stats->total_jobs ?? 0) > 0, ]); } -} \ No newline at end of file +} diff --git a/app/Livewire/CreateApplication.php b/app/Livewire/CreateApplication.php index 4cf886c..5af0dd0 100644 --- a/app/Livewire/CreateApplication.php +++ b/app/Livewire/CreateApplication.php @@ -2,9 +2,10 @@ namespace App\Livewire; -use Livewire\Component; -use Livewire\Attributes\Validate; +use App\Enums\ApplicationStatus; use App\Models\JobPosting; +use Livewire\Attributes\Validate; +use Livewire\Component; class CreateApplication extends Component { @@ -30,7 +31,7 @@ public function save() 'company' => $this->company, 'source_url' => $this->url, 'description' => $this->description, - 'status' => \App\Enums\ApplicationStatus::Saved->value, + 'status' => ApplicationStatus::Saved->value, ]); session()->flash('success', 'Job posting saved successfully.'); diff --git a/app/Livewire/CreateMatchReport.php b/app/Livewire/CreateMatchReport.php index 23f1f0d..dad1596 100644 --- a/app/Livewire/CreateMatchReport.php +++ b/app/Livewire/CreateMatchReport.php @@ -4,19 +4,23 @@ namespace App\Livewire; -use Livewire\Component; -use App\Models\Resume; use App\Models\JobPosting; +use App\Models\Resume; use App\Services\MatchAnalysisService; +use Livewire\Component; class CreateMatchReport extends Component { public $resume_id = ''; + public $job_posting_id = ''; + public $searchJob = ''; + public $searchResume = ''; public $preselectedJob = false; + public $isModal = false; // This method automatically runs when the component is loaded. @@ -33,7 +37,7 @@ public function mount($jobPostingId = null) public function generate(MatchAnalysisService $matchService) { $this->validate([ - 'resume_id' => 'required|exists:resumes,id', + 'resume_id' => 'required|exists:resumes,id', 'job_posting_id' => 'required|exists:job_postings,id', ]); @@ -48,7 +52,7 @@ public function generate(MatchAnalysisService $matchService) : 'Match report successfully generated.'; session()->flash('success', $message); - + return redirect()->route('matches.show', $result['report']); } @@ -58,8 +62,8 @@ public function render() $jobs = JobPosting::where('user_id', auth()->id()) ->when($this->searchJob, function ($q) { $q->where(function ($subQ) { - $subQ->where('title', 'like', '%' . $this->searchJob . '%') - ->orWhere('company', 'like', '%' . $this->searchJob . '%'); + $subQ->where('title', 'like', '%'.$this->searchJob.'%') + ->orWhere('company', 'like', '%'.$this->searchJob.'%'); }); }) ->latest() @@ -67,7 +71,7 @@ public function render() $resumes = Resume::where('user_id', auth()->id()) ->when($this->searchResume, function ($q) { - $q->where('label', 'like', '%' . $this->searchResume . '%'); + $q->where('label', 'like', '%'.$this->searchResume.'%'); }) ->orderByDesc('is_primary') // Puts Primary Resume at the very top ->latest() @@ -75,7 +79,7 @@ public function render() return view('livewire.create-match-report', [ 'resumes' => $resumes, - 'jobs' => $jobs, + 'jobs' => $jobs, ]); } -} \ No newline at end of file +} diff --git a/app/Livewire/EditApplication.php b/app/Livewire/EditApplication.php index 4434537..02909dc 100644 --- a/app/Livewire/EditApplication.php +++ b/app/Livewire/EditApplication.php @@ -2,9 +2,9 @@ namespace App\Livewire; -use Livewire\Component; -use Livewire\Attributes\Validate; use App\Models\JobPosting; +use Livewire\Attributes\Validate; +use Livewire\Component; class EditApplication extends Component { @@ -49,4 +49,4 @@ public function render() { return view('livewire.edit-application'); } -} \ No newline at end of file +} diff --git a/app/Livewire/InterviewIndex.php b/app/Livewire/InterviewIndex.php index 8f89ca7..82ffa26 100644 --- a/app/Livewire/InterviewIndex.php +++ b/app/Livewire/InterviewIndex.php @@ -4,10 +4,10 @@ namespace App\Livewire; +use App\Models\MockInterview; use Livewire\Attributes\Title; use Livewire\Component; use Livewire\WithPagination; -use App\Models\MockInterview; #[Title('Interview Prep')] class InterviewIndex extends Component @@ -15,7 +15,9 @@ class InterviewIndex extends Component use WithPagination; public $search = ''; + public $sort = 'newest'; + public $interviewToDelete = null; public function updatingSearch() @@ -47,8 +49,8 @@ public function render() if ($this->search) { $query->whereHas('jobPosting', function ($q) { - $q->where('title', 'like', '%' . $this->search . '%') - ->orWhere('company', 'like', '%' . $this->search . '%'); + $q->where('title', 'like', '%'.$this->search.'%') + ->orWhere('company', 'like', '%'.$this->search.'%'); }); } @@ -64,4 +66,4 @@ public function render() 'latestInterview' => MockInterview::where('user_id', auth()->id())->latest()->first(), ]); } -} \ No newline at end of file +} diff --git a/app/Livewire/InterviewPrep.php b/app/Livewire/InterviewPrep.php index d4f2a42..8a817ef 100644 --- a/app/Livewire/InterviewPrep.php +++ b/app/Livewire/InterviewPrep.php @@ -2,29 +2,30 @@ namespace App\Livewire; -use Livewire\Component; use App\Models\MatchReport; use App\Models\MockInterview; use App\Services\GroqMockInterviewService; use Exception; +use Livewire\Component; class InterviewPrep extends Component { public MatchReport $matchReport; + public array $questions = []; public function mount(MatchReport $matchReport) { $this->matchReport = $matchReport; - + if ($this->matchReport->user_id !== auth()->id()) { abort(403); } - // Properly check using the relation IDs instead of direct column names + // MatchReport uses 'job_id', while MockInterview uses 'job_posting_id' $existingInterview = MockInterview::where('user_id', auth()->id()) - ->where('resume_id', $this->matchReport->resume->id) - ->where('job_posting_id', $this->matchReport->jobPosting->id) + ->where('resume_id', $this->matchReport->resume_id) + ->where('job_posting_id', $this->matchReport->job_id) ->first(); if ($existingInterview) { @@ -40,11 +41,10 @@ public function generateQuestions(GroqMockInterviewService $service) $this->questions = $service->generateQuestions($resumeText, $jobText); - // Properly save using the relation IDs MockInterview::create([ 'user_id' => auth()->id(), - 'job_posting_id' => $this->matchReport->jobPosting->id, - 'resume_id' => $this->matchReport->resume->id, + 'job_posting_id' => $this->matchReport->job_id, // Updated to read job_id + 'resume_id' => $this->matchReport->resume_id, 'questions' => $this->questions, ]); } catch (Exception $e) { diff --git a/app/Livewire/MatchReportIndex.php b/app/Livewire/MatchReportIndex.php index 9f03a49..19c4f29 100644 --- a/app/Livewire/MatchReportIndex.php +++ b/app/Livewire/MatchReportIndex.php @@ -2,10 +2,10 @@ namespace App\Livewire; -use Livewire\Component; -use Livewire\WithPagination; use App\Models\MatchReport; use Livewire\Attributes\Url; +use Livewire\Component; +use Livewire\WithPagination; class MatchReportIndex extends Component { @@ -14,12 +14,12 @@ class MatchReportIndex extends Component // We use the #[Url] attribute so these stay in the address bar when refreshed #[Url] public $search = ''; - + #[Url] public $sort = 'newest'; - + public $reportToDelete = null; - + // This variable controls if the modal is open public $showCreateModal = false; @@ -51,9 +51,9 @@ public function executeDelete() if ($this->reportToDelete) { $report = MatchReport::where('user_id', auth()->id())->findOrFail($this->reportToDelete); $report->delete(); - + $this->reportToDelete = null; - + session()->flash('success', 'Match report deleted successfully.'); $this->dispatch('notify', message: 'Match report deleted successfully.'); } @@ -81,17 +81,17 @@ public function render() ->when($this->search, function ($query) { $query->where(function ($q) { $q->whereHas('jobPosting', function ($subQuery) { - $subQuery->where('title', 'like', '%' . $this->search . '%') - ->orWhere('company', 'like', '%' . $this->search . '%'); + $subQuery->where('title', 'like', '%'.$this->search.'%') + ->orWhere('company', 'like', '%'.$this->search.'%'); })->orWhereHas('resume', function ($subQuery) { - $subQuery->where('label', 'like', '%' . $this->search . '%'); + $subQuery->where('label', 'like', '%'.$this->search.'%'); }); }); }) - ->when($this->sort === 'newest', fn($q) => $q->latest()) - ->when($this->sort === 'oldest', fn($q) => $q->oldest()) - ->when($this->sort === 'score_high', fn($q) => $q->orderByDesc('score')) - ->when($this->sort === 'score_low', fn($q) => $q->orderBy('score')) + ->when($this->sort === 'newest', fn ($q) => $q->latest()) + ->when($this->sort === 'oldest', fn ($q) => $q->oldest()) + ->when($this->sort === 'score_high', fn ($q) => $q->orderByDesc('score')) + ->when($this->sort === 'score_low', fn ($q) => $q->orderBy('score')) ->paginate(9); return view('livewire.match-report-index', [ @@ -101,4 +101,4 @@ public function render() 'highestScore' => (int) ($stats->highest_score ?? 0), ]); } -} \ No newline at end of file +} diff --git a/app/Livewire/ResumeIndex.php b/app/Livewire/ResumeIndex.php index 7bf2654..4e15221 100644 --- a/app/Livewire/ResumeIndex.php +++ b/app/Livewire/ResumeIndex.php @@ -4,13 +4,13 @@ namespace App\Livewire; -use Livewire\Component; -use Livewire\WithFileUploads; -use Livewire\WithPagination; use App\Models\Resume; use App\Services\ResumeParserService; -use Livewire\Attributes\Validate; use Livewire\Attributes\Url; +use Livewire\Attributes\Validate; +use Livewire\Component; +use Livewire\WithFileUploads; +use Livewire\WithPagination; class ResumeIndex extends Component { @@ -45,15 +45,15 @@ public function save(ResumeParserService $parser) $uploadResult = $parser->processUpload($this->file); Resume::create([ - 'user_id' => auth()->id(), - 'label' => $this->label, - 'file_url' => $uploadResult['file_url'], + 'user_id' => auth()->id(), + 'label' => $this->label, + 'file_url' => $uploadResult['file_url'], 'content_raw' => $uploadResult['content_raw'], - 'is_primary' => false, + 'is_primary' => false, ]); $this->reset(['label', 'file']); - + $this->dispatch('close-slide-over'); $this->dispatch('notify', message: 'Resume uploaded and parsed successfully.'); } @@ -64,10 +64,10 @@ public function togglePrimary($id) if ($resume) { $wasPrimary = $resume->is_primary; - + Resume::where('user_id', auth()->id())->update(['is_primary' => false]); - - if (!$wasPrimary) { + + if (! $wasPrimary) { $resume->update(['is_primary' => true]); $this->dispatch('notify', message: 'Primary resume set.'); } else { @@ -101,7 +101,7 @@ public function render() $resumes = (clone $baseQuery) ->when($this->search, function ($query) { - $searchTerm = '%' . trim($this->search) . '%'; + $searchTerm = '%'.trim($this->search).'%'; $query->where('label', 'like', $searchTerm); }) ->when($this->sort === 'newest', fn ($q) => $q->latest()) @@ -117,4 +117,4 @@ public function render() 'latestResume' => $latestResume, ]); } -} \ No newline at end of file +} diff --git a/app/Mail/ContactSupportMessage.php b/app/Mail/ContactSupportMessage.php index be6a06f..49d1fd9 100644 --- a/app/Mail/ContactSupportMessage.php +++ b/app/Mail/ContactSupportMessage.php @@ -4,10 +4,10 @@ use Illuminate\Bus\Queueable; use Illuminate\Mail\Mailable; +use Illuminate\Mail\Mailables\Address; use Illuminate\Mail\Mailables\Content; use Illuminate\Mail\Mailables\Envelope; use Illuminate\Queue\SerializesModels; -use Illuminate\Mail\Mailables\Address; class ContactSupportMessage extends Mailable { @@ -27,7 +27,7 @@ public function envelope(): Envelope replyTo: [ new Address($this->data['email'], $this->data['name']), ], - subject: 'Grit Support Request: ' . $this->data['name'], + subject: 'Grit Support Request: '.$this->data['name'], ); } @@ -37,4 +37,4 @@ public function content(): Content view: 'emails.contact-support', ); } -} \ No newline at end of file +} diff --git a/app/Models/JobPosting.php b/app/Models/JobPosting.php index c68bb7c..f8747b0 100644 --- a/app/Models/JobPosting.php +++ b/app/Models/JobPosting.php @@ -2,10 +2,10 @@ namespace App\Models; +use App\Enums\ApplicationStatus; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; -use App\Enums\ApplicationStatus; class JobPosting extends Model { @@ -33,4 +33,4 @@ public function user(): BelongsTo { return $this->belongsTo(User::class); } -} \ No newline at end of file +} diff --git a/app/Models/MockInterview.php b/app/Models/MockInterview.php index cf43216..f58a711 100644 --- a/app/Models/MockInterview.php +++ b/app/Models/MockInterview.php @@ -35,4 +35,4 @@ public function resume(): BelongsTo { return $this->belongsTo(Resume::class); } -} \ No newline at end of file +} diff --git a/app/Models/Resume.php b/app/Models/Resume.php index a7c86e0..4cae7e2 100644 --- a/app/Models/Resume.php +++ b/app/Models/Resume.php @@ -21,4 +21,4 @@ public function user() { return $this->belongsTo(User::class); } -} \ No newline at end of file +} diff --git a/app/Models/User.php b/app/Models/User.php index 6bc22f8..33e928e 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -31,4 +31,4 @@ protected function casts(): array 'password' => 'hashed', ]; } -} \ No newline at end of file +} diff --git a/app/Policies/MatchReportPolicy.php b/app/Policies/MatchReportPolicy.php index 728ae0d..aed5e20 100644 --- a/app/Policies/MatchReportPolicy.php +++ b/app/Policies/MatchReportPolicy.php @@ -21,4 +21,4 @@ public function delete(User $user, MatchReport $matchReport): bool { return $user->id === $matchReport->user_id; } -} \ No newline at end of file +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 8805893..cf99e42 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,8 +2,8 @@ namespace App\Providers; -use Illuminate\Support\ServiceProvider; use Illuminate\Support\Facades\URL; +use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { @@ -14,7 +14,7 @@ public function register(): void public function boot(): void { - if (env('APP_ENV') === 'production') { + if (config('app.env') === 'production') { URL::forceScheme('https'); } } diff --git a/app/Services/GritActionPlanService.php b/app/Services/GritActionPlanService.php index 4effb99..1e12cec 100644 --- a/app/Services/GritActionPlanService.php +++ b/app/Services/GritActionPlanService.php @@ -3,43 +3,44 @@ namespace App\Services; use App\Models\MatchReport; +use Exception; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; -use Exception; class GritActionPlanService { public function generatePlan(MatchReport $matchReport): ?string { - $keywords = is_array($matchReport->missing_keywords) - ? $matchReport->missing_keywords - : json_decode($matchReport->missing_keywords ?? '[]', true); + // Handle both Laravel array casts and raw JSON strings + $keywords = is_string($matchReport->missing_keywords) + ? json_decode($matchReport->missing_keywords, true) + : ($matchReport->missing_keywords ?? []); if (empty($keywords)) { return json_encode([ - "steps" => [ + 'steps' => [ [ - "title" => "Great Job!", - "actions" => ["You already meet all the key requirements for this role. Keep up the great work!"] - ] - ] + 'title' => 'Great Job!', + 'actions' => ['You already meet all the key requirements for this role. Keep up the great work!'], + ], + ], ]); } - $prompt = "You are an expert career mentor. The user is missing these skills: " . implode(', ', $keywords) . ".\n\n" . - "Create a 3-step action plan to learn these skills. You MUST respond ONLY in valid JSON format.\n" . - "Use this exact schema:\n" . - "{\n" . - " \"steps\": [\n" . - " {\n" . - " \"title\": \"Step 1: [Actionable Title]\",\n" . - " \"actions\": [\n" . - " \"First actionable advice. Use **bold text** for tools.\",\n" . - " \"Second actionable advice. Use **bold text** for concepts.\"\n" . - " ]\n" . - " }\n" . - " ]\n" . - "}"; + $prompt = 'You are an expert career mentor. The user is missing these skills: '.implode(', ', $keywords).".\n\n". + "Create a 3-step action plan to learn these skills. You MUST respond ONLY in valid JSON format.\n". + "Use this exact schema:\n". + "{\n". + " \"steps\": [\n". + " {\n". + " \"title\": \"Step 1: [Actionable Title]\",\n". + " \"actions\": [\n". + " \"First actionable advice. Use **bold text** for tools.\",\n". + " \"Second actionable advice. Use **bold text** for concepts.\"\n". + " ]\n". + " }\n". + " ]\n". + '}'; $response = Http::withToken(config('services.groq.api_key')) ->withHeaders(['Content-Type' => 'application/json']) @@ -56,16 +57,16 @@ public function generatePlan(MatchReport $matchReport): ?string if ($response->successful()) { $plan = $response->json('choices.0.message.content'); - + $matchReport->update([ - 'action_plan' => $plan + 'action_plan' => $plan, ]); return $plan; } - Log::error('Groq Action Plan Error: ' . $response->body()); - + Log::error('Groq Action Plan Error: '.$response->body()); + throw new Exception('Failed to generate action plan from Groq API.'); } } \ No newline at end of file diff --git a/app/Services/GroqMockInterviewService.php b/app/Services/GroqMockInterviewService.php index 7f84d76..2c52de5 100644 --- a/app/Services/GroqMockInterviewService.php +++ b/app/Services/GroqMockInterviewService.php @@ -2,9 +2,9 @@ namespace App\Services; +use Exception; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; -use Exception; class GroqMockInterviewService { @@ -25,19 +25,19 @@ public function generateQuestions(string $resumeContent, string $jobDescription) 'model' => 'llama-3.3-70b-versatile', 'messages' => [ ['role' => 'system', 'content' => $prompt], - ['role' => 'user', 'content' => "Resume: \n" . $resumeContent . "\n\nJob Description: \n" . $jobDescription], + ['role' => 'user', 'content' => "Resume: \n".$resumeContent."\n\nJob Description: \n".$jobDescription], ], 'response_format' => ['type' => 'json_object'], ]); if ($response->failed()) { - Log::error('Groq API Error: ' . $response->body()); - throw new Exception('Groq API Error: ' . $response->status()); + Log::error('Groq API Error: '.$response->body()); + throw new Exception('Groq API Error: '.$response->status()); } $content = $response->json('choices.0.message.content'); $decoded = json_decode($content, true); - return $decoded['questions'] ?? []; + return $decoded['questions'] ?? []; } -} \ No newline at end of file +} diff --git a/app/Services/MatchAnalysisService.php b/app/Services/MatchAnalysisService.php index 1da77e2..ac141a5 100644 --- a/app/Services/MatchAnalysisService.php +++ b/app/Services/MatchAnalysisService.php @@ -4,19 +4,22 @@ namespace App\Services; -use App\Models\Resume; +use App\Jobs\GenerateMatchReport; use App\Models\JobPosting; use App\Models\MatchReport; -use App\Jobs\GenerateMatchReport; +use App\Models\Resume; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; -use Illuminate\Support\Facades\Cache; class MatchAnalysisService { private const GROQ_API_URL = 'https://api.groq.com/openai/v1/chat/completions'; + private const GROQ_MODEL = 'llama-3.3-70b-versatile'; + private const CACHE_TTL_DAYS = 7; + private const REPORT_CACHE_TTL_DAYS = 30; public function findOrCreateReport(int $resumeId, int $jobPostingId, int $userId): array @@ -24,26 +27,26 @@ public function findOrCreateReport(int $resumeId, int $jobPostingId, int $userId $resume = Resume::findOrFail($resumeId); $jobPosting = JobPosting::findOrFail($jobPostingId); - $fingerprint = hash('sha256', $resume->content_raw . $jobPosting->description); - $cacheKey = 'match_report_hash_' . $fingerprint; + $fingerprint = hash('sha256', $resume->content_raw.$jobPosting->description); + $cacheKey = 'match_report_hash_'.$fingerprint; if (Cache::has($cacheKey)) { $existingReport = MatchReport::find(Cache::get($cacheKey)); if ($existingReport) { return [ - 'report' => $existingReport, + 'report' => $existingReport, 'is_cached' => true, ]; } } $matchReport = MatchReport::create([ - 'user_id' => $userId, + 'user_id' => $userId, 'resume_id' => $resume->id, - 'job_id' => $jobPosting->id, - 'score' => 0, - 'status' => 'processing', + 'job_id' => $jobPosting->id, + 'score' => 0, + 'status' => 'processing', ]); Cache::put($cacheKey, $matchReport->id, now()->addDays(self::REPORT_CACHE_TTL_DAYS)); @@ -51,20 +54,20 @@ public function findOrCreateReport(int $resumeId, int $jobPostingId, int $userId GenerateMatchReport::dispatch($matchReport); return [ - 'report' => $matchReport, + 'report' => $matchReport, 'is_cached' => false, ]; } public function analyze(Resume $resume, JobPosting $jobPosting): array { - $cacheKey = 'match_report_' . $resume->id . '_' . $jobPosting->id; + $cacheKey = 'match_report_'.$resume->id.'_'.$jobPosting->id; return Cache::remember($cacheKey, now()->addDays(self::CACHE_TTL_DAYS), function () use ($resume, $jobPosting) { try { - $apiKey = env('GROQ_API_KEY'); + $apiKey = config('services.groq.api_key'); - if (!$apiKey) { + if (! $apiKey) { throw new \Exception('Missing API Key'); } @@ -72,9 +75,9 @@ public function analyze(Resume $resume, JobPosting $jobPosting): array ->withHeaders(['Content-Type' => 'application/json']) ->timeout(15) ->post(self::GROQ_API_URL, [ - 'model' => self::GROQ_MODEL, + 'model' => self::GROQ_MODEL, 'response_format' => ['type' => 'json_object'], - 'messages' => [ + 'messages' => [ ['role' => 'system', 'content' => $this->buildSystemPrompt()], ['role' => 'user', 'content' => $this->buildUserPrompt($resume, $jobPosting)], ], @@ -82,12 +85,13 @@ public function analyze(Resume $resume, JobPosting $jobPosting): array ]); if ($response->failed()) { - throw new \Exception('API Call Failed: ' . $response->body()); + throw new \Exception('API Call Failed: '.$response->body()); } return $this->parseApiResponse($response); } catch (\Exception $e) { - Log::error('Groq API error. Using offline fallback. Error: ' . $e->getMessage()); + Log::error('Groq API error. Using offline fallback. Error: '.$e->getMessage()); + return $this->offlineFallback($resume, $jobPosting); } }); @@ -97,20 +101,20 @@ private function parseApiResponse($response): array { $data = json_decode($response->json('choices.0.message.content') ?? '{}', true); - if (!isset($data['score'])) { + if (! isset($data['score'])) { throw new \Exception('Invalid JSON from AI'); } return [ - 'score' => (int) $data['score'], + 'score' => (int) $data['score'], 'missing_keywords' => $data['missing_keywords'] ?? [], - 'reasoning' => $data['reasoning'] ?? 'No reasoning provided.', + 'reasoning' => $data['reasoning'] ?? 'No reasoning provided.', ]; } private function buildSystemPrompt(): string { - return "You are a strict Applicant Tracking System. + return 'You are a strict Applicant Tracking System. Follow these steps: 1. Extract requirements from the Job Description. Include hard skills, required degrees, and soft skills if it is an entry level role. @@ -119,16 +123,16 @@ private function buildSystemPrompt(): string You must respond ONLY with a valid JSON object using exactly this format: { - \"extracted_requirements\": [\"requirement1\", \"requirement2\"], - \"missing_keywords\": [\"missing1\", \"missing2\"], - \"score\": 50, - \"reasoning\": \"A two sentence explanation focusing on matches and gaps.\" -}"; + "extracted_requirements": ["requirement1", "requirement2"], + "missing_keywords": ["missing1", "missing2"], + "score": 50, + "reasoning": "A two sentence explanation focusing on matches and gaps." +}'; } private function buildUserPrompt(Resume $resume, JobPosting $jobPosting): string { - return "Resume:\n" . ($resume->content_raw ?? '') . "\n\nJob Description:\n" . ($jobPosting->description ?? ''); + return "Resume:\n".($resume->content_raw ?? '')."\n\nJob Description:\n".($jobPosting->description ?? ''); } private function offlineFallback(Resume $resume, JobPosting $jobPosting): array @@ -155,9 +159,9 @@ private function offlineFallback(Resume $resume, JobPosting $jobPosting): array $score = ($matchCount / $totalWords) * 100; return [ - 'score' => (int) round($score), + 'score' => (int) round($score), 'missing_keywords' => array_merge(['(Offline Backup)'], $missingKeywords), - 'reasoning' => 'Offline fallback used due to AI timeout or failure. The score is based on a simple word overlap algorithm.', + 'reasoning' => 'Offline fallback used due to AI timeout or failure. The score is based on a simple word overlap algorithm.', ]; } } diff --git a/app/Services/ResumeParserService.php b/app/Services/ResumeParserService.php index 458d420..c528049 100644 --- a/app/Services/ResumeParserService.php +++ b/app/Services/ResumeParserService.php @@ -20,13 +20,14 @@ public function __construct(?Parser $parser = null) public function parse(string $filePath): ?string { try { - $pdfParser = $this->parser ?? new Parser(); + $pdfParser = $this->parser ?? new Parser; $pdf = $pdfParser->parseFile($filePath); $rawText = $pdf->getText(); return trim(mb_convert_encoding($rawText, 'UTF-8', 'UTF-8')); } catch (Exception $e) { - Log::error('Resume standard parsing failed: ' . $e->getMessage()); + Log::error('Resume standard parsing failed: '.$e->getMessage()); + return null; } } @@ -53,8 +54,8 @@ public function processUpload(object $file): array } return [ - 'file_url' => $cloudinaryResponse['secure_url'], + 'file_url' => $cloudinaryResponse['secure_url'], 'content_raw' => $rawText, ]; } -} \ No newline at end of file +} diff --git a/app/Support/Csp/CustomPolicy.php b/app/Support/Csp/CustomPolicy.php index d77c4b3..37050a9 100644 --- a/app/Support/Csp/CustomPolicy.php +++ b/app/Support/Csp/CustomPolicy.php @@ -26,7 +26,7 @@ public function configure(Policy $policy): void ->add(Directive::FONT, [ Keyword::SELF, 'fonts.gstatic.com', - 'data:', + 'data:', ]) ->add(Directive::SCRIPT, [ Keyword::SELF, @@ -34,4 +34,4 @@ public function configure(Policy $policy): void Keyword::UNSAFE_EVAL, ]); } -} \ No newline at end of file +} diff --git a/bootstrap/app.php b/bootstrap/app.php index 4d19aac..1d69a6e 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -3,6 +3,7 @@ use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Middleware; +use Spatie\Csp\AddCspHeaders; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( @@ -12,8 +13,8 @@ ) ->withMiddleware(function (Middleware $middleware) { $middleware->trustProxies(at: '*'); - $middleware->append(\Spatie\Csp\AddCspHeaders::class); + $middleware->append(AddCspHeaders::class); }) ->withExceptions(function (Exceptions $exceptions) { // - })->create(); \ No newline at end of file + })->create(); diff --git a/bootstrap/providers.php b/bootstrap/providers.php index 4c29e23..95629a9 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -1,6 +1,9 @@ [ 'cloud_name' => env('CLOUDINARY_CLOUD_NAME'), - 'api_key' => env('CLOUDINARY_API_KEY'), + 'api_key' => env('CLOUDINARY_API_KEY'), 'api_secret' => env('CLOUDINARY_API_SECRET'), ], @@ -16,4 +16,4 @@ 'upload_preset' => env('CLOUDINARY_UPLOAD_PRESET', 'grit_uploads'), 'folder' => env('CLOUDINARY_FOLDER', 'grit_uploads'), -]; \ No newline at end of file +]; diff --git a/config/csp.php b/config/csp.php index 6239a3d..673b88d 100644 --- a/config/csp.php +++ b/config/csp.php @@ -1,5 +1,9 @@ [ - Spatie\Csp\Presets\Basic::class, - App\Support\Csp\CustomPolicy::class, + Basic::class, + CustomPolicy::class, ], /** @@ -52,7 +56,7 @@ /* * The class responsible for generating the nonces used in inline tags and headers. */ - 'nonce_generator' => Spatie\Csp\Nonce\RandomString::class, + 'nonce_generator' => RandomString::class, /* * Set false to disable automatic nonce generation and handling. diff --git a/config/sanctum.php b/config/sanctum.php index 44527d6..cde73cf 100644 --- a/config/sanctum.php +++ b/config/sanctum.php @@ -1,5 +1,8 @@ [ - 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, - 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class, - 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, + 'authenticate_session' => AuthenticateSession::class, + 'encrypt_cookies' => EncryptCookies::class, + 'validate_csrf_token' => ValidateCsrfToken::class, ], ]; diff --git a/config/session.php b/config/session.php index 086809b..2267e76 100644 --- a/config/session.php +++ b/config/session.php @@ -230,4 +230,4 @@ 'serialization' => 'json', -]; \ No newline at end of file +]; diff --git a/database/factories/JobPostingFactory.php b/database/factories/JobPostingFactory.php index 6ef1c37..abf6bce 100644 --- a/database/factories/JobPostingFactory.php +++ b/database/factories/JobPostingFactory.php @@ -4,9 +4,9 @@ namespace Database\Factories; +use App\Enums\ApplicationStatus; use App\Models\JobPosting; use App\Models\User; -use App\Enums\ApplicationStatus; use Illuminate\Database\Eloquent\Factories\Factory; class JobPostingFactory extends Factory @@ -16,12 +16,12 @@ class JobPostingFactory extends Factory public function definition(): array { return [ - 'user_id' => User::factory(), - 'title' => fake()->jobTitle(), - 'company' => fake()->company(), + 'user_id' => User::factory(), + 'title' => fake()->jobTitle(), + 'company' => fake()->company(), 'description' => fake()->paragraphs(3, true), - 'source_url' => fake()->url(), - 'status' => ApplicationStatus::Saved->value, + 'source_url' => fake()->url(), + 'status' => ApplicationStatus::Saved->value, ]; } -} \ No newline at end of file +} diff --git a/database/factories/MatchReportFactory.php b/database/factories/MatchReportFactory.php index 501b40b..759068e 100644 --- a/database/factories/MatchReportFactory.php +++ b/database/factories/MatchReportFactory.php @@ -4,9 +4,9 @@ namespace Database\Factories; +use App\Models\JobPosting; use App\Models\MatchReport; use App\Models\Resume; -use App\Models\JobPosting; use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; @@ -17,13 +17,13 @@ class MatchReportFactory extends Factory public function definition(): array { return [ - 'user_id' => User::factory(), - 'resume_id' => Resume::factory(), - 'job_id' => JobPosting::factory(), - 'score' => fake()->numberBetween(0, 100), + 'user_id' => User::factory(), + 'resume_id' => Resume::factory(), + 'job_id' => JobPosting::factory(), + 'score' => fake()->numberBetween(0, 100), 'missing_keywords' => ['React', 'Docker'], - 'reasoning' => fake()->sentence(), - 'status' => 'processing', + 'reasoning' => fake()->sentence(), + 'status' => 'processing', ]; } } diff --git a/database/factories/ResumeFactory.php b/database/factories/ResumeFactory.php index f0627bd..a1195e1 100644 --- a/database/factories/ResumeFactory.php +++ b/database/factories/ResumeFactory.php @@ -15,11 +15,11 @@ class ResumeFactory extends Factory public function definition(): array { return [ - 'user_id' => User::factory(), - 'label' => fake()->sentence(3), - 'file_url' => 'https://res.cloudinary.com/test/upload/v1/grit_uploads/' . fake()->uuid() . '.pdf', + 'user_id' => User::factory(), + 'label' => fake()->sentence(3), + 'file_url' => 'https://res.cloudinary.com/test/upload/v1/grit_uploads/'.fake()->uuid().'.pdf', 'content_raw' => fake()->paragraphs(3, true), - 'is_active' => true, + 'is_active' => true, ]; } } diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 9e13942..8b2a8e5 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -2,7 +2,6 @@ namespace Database\Factories; -use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; @@ -20,4 +19,4 @@ public function definition(): array 'remember_token' => Str::random(10), ]; } -} \ No newline at end of file +} diff --git a/database/migrations/2026_03_21_000002_create_job_postings_table.php b/database/migrations/2026_03_21_000002_create_job_postings_table.php index 5d82418..a72e565 100644 --- a/database/migrations/2026_03_21_000002_create_job_postings_table.php +++ b/database/migrations/2026_03_21_000002_create_job_postings_table.php @@ -16,7 +16,7 @@ public function up(): void $table->longText('description')->nullable(); $table->string('source_url')->nullable(); // Default changed from 'draft' to 'saved' - $table->string('status')->default('saved'); + $table->string('status')->default('saved'); $table->timestamps(); }); } @@ -25,4 +25,4 @@ public function down(): void { Schema::dropIfExists('job_postings'); } -}; \ No newline at end of file +}; diff --git a/database/migrations/2026_03_27_012026_add_status_to_match_reports_table.php b/database/migrations/2026_03_27_012026_add_status_to_match_reports_table.php index 949f918..cfdbec2 100644 --- a/database/migrations/2026_03_27_012026_add_status_to_match_reports_table.php +++ b/database/migrations/2026_03_27_012026_add_status_to_match_reports_table.php @@ -19,4 +19,4 @@ public function down(): void $table->dropColumn('status'); }); } -}; \ No newline at end of file +}; diff --git a/database/migrations/2026_03_27_122414_change_description_to_text_on_job_postings_table.php b/database/migrations/2026_03_27_122414_change_description_to_text_on_job_postings_table.php index 6c3362a..3c8f953 100644 --- a/database/migrations/2026_03_27_122414_change_description_to_text_on_job_postings_table.php +++ b/database/migrations/2026_03_27_122414_change_description_to_text_on_job_postings_table.php @@ -20,4 +20,4 @@ public function down(): void $table->string('description')->nullable()->change(); }); } -}; \ No newline at end of file +}; diff --git a/database/migrations/2026_03_30_182619_add_is_primary_to_resumes_table.php b/database/migrations/2026_03_30_182619_add_is_primary_to_resumes_table.php index bb9462e..da67130 100644 --- a/database/migrations/2026_03_30_182619_add_is_primary_to_resumes_table.php +++ b/database/migrations/2026_03_30_182619_add_is_primary_to_resumes_table.php @@ -19,4 +19,4 @@ public function down(): void $table->dropColumn('is_primary'); }); } -}; \ No newline at end of file +}; diff --git a/database/migrations/2026_03_31_082258_add_action_plan_to_match_reports_table.php b/database/migrations/2026_03_31_082258_add_action_plan_to_match_reports_table.php index 18d6983..fe6e116 100644 --- a/database/migrations/2026_03_31_082258_add_action_plan_to_match_reports_table.php +++ b/database/migrations/2026_03_31_082258_add_action_plan_to_match_reports_table.php @@ -19,4 +19,4 @@ public function down(): void $table->dropColumn('action_plan'); }); } -}; \ No newline at end of file +}; diff --git a/database/migrations/2026_04_01_114337_create_mock_interviews_table.php b/database/migrations/2026_04_01_114337_create_mock_interviews_table.php index c5b0c34..0090f43 100644 --- a/database/migrations/2026_04_01_114337_create_mock_interviews_table.php +++ b/database/migrations/2026_04_01_114337_create_mock_interviews_table.php @@ -14,7 +14,7 @@ public function up(): void $table->foreignId('job_posting_id')->constrained()->cascadeOnDelete(); $table->foreignId('resume_id')->constrained()->cascadeOnDelete(); // Both SQLite and PostgreSQL support JSON columns in Laravel - $table->json('questions'); + $table->json('questions'); $table->timestamps(); }); } diff --git a/database/migrations/2026_04_07_051047_drop_api_token_from_users_table.php b/database/migrations/2026_04_07_051047_drop_api_token_from_users_table.php index b225507..69b5504 100644 --- a/database/migrations/2026_04_07_051047_drop_api_token_from_users_table.php +++ b/database/migrations/2026_04_07_051047_drop_api_token_from_users_table.php @@ -21,4 +21,4 @@ public function down(): void $table->string('api_token', 80)->unique()->nullable()->default(null); }); } -}; \ No newline at end of file +}; diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..0009d12 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,20 @@ +includes: + - vendor/larastan/larastan/extension.neon + +parameters: + paths: + - app/ + - bootstrap/ + - config/ + - database/ + - routes/ + - resources/views/ + + # Level 5 is the industry standard balance between strictness and pragmatism for existing Laravel apps + level: 5 + + # Exclude auto-generated files that do not require strict analysis + excludePaths: + - bootstrap/cache/* + - storage/* + - vendor/* \ No newline at end of file diff --git a/resources/views/components/faq-section.blade.php b/resources/views/components/faq-section.blade.php index ea9ba69..917afdc 100644 --- a/resources/views/components/faq-section.blade.php +++ b/resources/views/components/faq-section.blade.php @@ -106,7 +106,7 @@ -