diff --git a/job-board/app/Http/Controllers/EmployerController.php b/job-board/app/Http/Controllers/EmployerController.php new file mode 100644 index 0000000..5cb0f2f --- /dev/null +++ b/job-board/app/Http/Controllers/EmployerController.php @@ -0,0 +1,69 @@ +authorizeResource(Employer::class); + } + + /** + * Show the form for creating a new resource. + */ + public function create() + { + return view('employer.create'); + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + auth()->user()->employer()->create( + $request->validate([ + 'company_name' => 'required|min:3|unique:employers,company_name' + ]) + ); + + return redirect()->route('my-jobs.index') + ->with('success', 'Your employer account was created. You can post your first job.'); + } + + /** + * Display the specified resource. + */ + public function show(string $id) + { + // + } + + /** + * Show the form for editing the specified resource. + */ + public function edit(string $id) + { + // + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, string $id) + { + // + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(string $id) + { + // + } +} \ No newline at end of file diff --git a/job-board/app/Http/Controllers/JobApplicationController.php b/job-board/app/Http/Controllers/JobApplicationController.php index 8eb4bce..8b95be8 100644 --- a/job-board/app/Http/Controllers/JobApplicationController.php +++ b/job-board/app/Http/Controllers/JobApplicationController.php @@ -16,11 +16,19 @@ public function create(Job $job) public function store(Job $job, Request $request) { $this->authorize('apply', $job); + + $validatedData = $request->validate([ + 'expected_salary' => 'required|min:1|max:1000000', + 'cv' => 'required|file|mimes:pdf|max:2048', + ]); + + $file = $request->file('cv'); + $path = $file->store('cvs', 'private'); + $job->jobApplications()->create([ 'user_id' => $request->user()->id, - ...$request->validate([ - 'expected_salary' => 'required|min:1|max:1000000' - ]) + 'cv_path' => $path, + 'expected_salary' => $validatedData['expected_salary'], ]); return redirect()->route('jobs.show', $job) diff --git a/job-board/app/Http/Controllers/JobController.php b/job-board/app/Http/Controllers/JobController.php index 7e398c1..120fad5 100644 --- a/job-board/app/Http/Controllers/JobController.php +++ b/job-board/app/Http/Controllers/JobController.php @@ -7,9 +7,6 @@ class JobController extends Controller { - /** - * Display a listing of the resource. - */ public function index() { $filters = request()->only( @@ -22,29 +19,10 @@ public function index() return view( 'job.index', - ['jobs' => Job::with('employer')->filter($filters)->get()] + ['jobs' => Job::with('employer')->filter($filters)->latest()->get()] ); } - /** - * Show the form for creating a new resource. - */ - public function create() - { - // - } - - /** - * Store a newly created resource in storage. - */ - public function store(Request $request) - { - // - } - - /** - * Display the specified resource. - */ public function show(Job $job) { return view( @@ -52,28 +30,4 @@ public function show(Job $job) ['job' => $job->load('employer.jobs')] ); } - - /** - * Show the form for editing the specified resource. - */ - public function edit(string $id) - { - // - } - - /** - * Update the specified resource in storage. - */ - public function update(Request $request, string $id) - { - // - } - - /** - * Remove the specified resource from storage. - */ - public function destroy(string $id) - { - // - } } \ No newline at end of file diff --git a/job-board/app/Http/Controllers/MyJobApplicationController.php b/job-board/app/Http/Controllers/MyJobApplicationController.php index da03bd6..7dbbad5 100644 --- a/job-board/app/Http/Controllers/MyJobApplicationController.php +++ b/job-board/app/Http/Controllers/MyJobApplicationController.php @@ -15,7 +15,8 @@ public function index() 'applications' => auth()->user()->jobApplications() ->with([ 'job' => fn($query) => $query->withCount('jobApplications') - ->withAvg('jobApplications', 'expected_salary'), + ->withAvg('jobApplications', 'expected_salary') + ->withTrashed(), 'job.employer' ]) ->latest()->get() diff --git a/job-board/app/Http/Controllers/MyJobController.php b/job-board/app/Http/Controllers/MyJobController.php new file mode 100644 index 0000000..fe9312f --- /dev/null +++ b/job-board/app/Http/Controllers/MyJobController.php @@ -0,0 +1,80 @@ +authorizeResource(Job::class, 'my_job'); + } + + public function index() + { + return view( + 'my_job.index', + [ + 'jobs' => auth()->user()->employer + ->jobs() + ->with(['employer', 'jobApplications', 'jobApplications.user']) + ->withTrashed() + ->get() + ] + ); + } + + public function create() + { + return view('my_job.create'); + } + + public function store(Request $request) + { + $validatedData = $request->validate([ + 'title' => 'required|string|max:255', + 'location' => 'required|string|max:255', + 'salary' => 'required|numeric|min:5000', + 'description' => 'required|string', + 'experience' => 'required|in:' . implode(',', Job::$experience), + 'category' => 'required|in:' . implode(',', Job::$category), + ]); + + $job = $request->user()->employer->jobs()->create($validatedData); + + return redirect()->route('my-jobs.index') + ->with('success', 'Job created successfully.'); + } + + public function edit(Job $myJob) + { + return view('my_job.edit', ['job' => $myJob]); + } + + public function update(Request $request, Job $myJob) + { + $validatedData = $request->validate([ + 'title' => 'required|string|max:255', + 'location' => 'required|string|max:255', + 'salary' => 'required|numeric|min:5000', + 'description' => 'required|string', + 'experience' => 'required|in:' . implode(',', Job::$experience), + 'category' => 'required|in:' . implode(',', Job::$category), + ]); + + $myJob->update($validatedData); + + return redirect()->route('my-jobs.index') + ->with('success', 'Job updated successfully.'); + } + + public function destroy(Job $myJob) + { + $myJob->delete(); + + return redirect()->route('my-jobs.index') + ->with('success', 'Job deleted successfully.'); + } +} \ No newline at end of file diff --git a/job-board/app/Http/Controllers/MyJobCvController.php b/job-board/app/Http/Controllers/MyJobCvController.php new file mode 100644 index 0000000..8b6b8e4 --- /dev/null +++ b/job-board/app/Http/Controllers/MyJobCvController.php @@ -0,0 +1,25 @@ +authorize('downloadCv', $jobApplication->job); + $path = $jobApplication->cv_path; + + if (!Storage::disk('private')->exists($path)) { + abort(Response::HTTP_NOT_FOUND, 'CV not found.'); + } + + $downloadName = 'cv-' . $jobApplication->id . '.pdf'; + + return Storage::disk('private')->download($path, $downloadName); + } +} \ No newline at end of file diff --git a/job-board/app/Http/Kernel.php b/job-board/app/Http/Kernel.php index 1fb53dc..d319305 100644 --- a/job-board/app/Http/Kernel.php +++ b/job-board/app/Http/Kernel.php @@ -14,7 +14,7 @@ class Kernel extends HttpKernel * @var array */ protected $middleware = [ - // \App\Http\Middleware\TrustHosts::class, + // \App\Http\Middleware\TrustHosts::class, \App\Http\Middleware\TrustProxies::class, \Illuminate\Http\Middleware\HandleCors::class, \App\Http\Middleware\PreventRequestsDuringMaintenance::class, @@ -39,8 +39,8 @@ class Kernel extends HttpKernel ], 'api' => [ - // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, - \Illuminate\Routing\Middleware\ThrottleRequests::class.':api', + // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, + \Illuminate\Routing\Middleware\ThrottleRequests::class . ':api', \Illuminate\Routing\Middleware\SubstituteBindings::class, ], ]; @@ -63,5 +63,6 @@ class Kernel extends HttpKernel 'signed' => \App\Http\Middleware\ValidateSignature::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, + 'employer' => \App\Http\Middleware\Employer::class ]; -} +} \ No newline at end of file diff --git a/job-board/app/Http/Middleware/Employer.php b/job-board/app/Http/Middleware/Employer.php new file mode 100644 index 0000000..3b94d13 --- /dev/null +++ b/job-board/app/Http/Middleware/Employer.php @@ -0,0 +1,27 @@ +user() || null === $request->user()->employer) { + return redirect()->route('employer.create') + ->with('error', 'You need to register as an employer first!'); + // throw new AuthorizationException('You are not a registered employer.'); + } + + return $next($request); + } +} \ No newline at end of file diff --git a/job-board/app/Models/Employer.php b/job-board/app/Models/Employer.php index 72defb6..2e2d2c2 100644 --- a/job-board/app/Models/Employer.php +++ b/job-board/app/Models/Employer.php @@ -11,6 +11,8 @@ class Employer extends Model { use HasFactory; + protected $fillable = ['company_name']; + public function jobs(): HasMany { return $this->hasMany(Job::class); diff --git a/job-board/app/Models/Job.php b/job-board/app/Models/Job.php index 6002a02..8141dd4 100644 --- a/job-board/app/Models/Job.php +++ b/job-board/app/Models/Job.php @@ -8,11 +8,14 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Query\Builder as QueryBuilder; class Job extends Model { - use HasFactory; + use HasFactory, SoftDeletes; + + protected $fillable = ['title', 'experience', 'category', 'salary', 'description', 'location']; public static array $experience = ['entry', 'intermediate', 'senior']; public static array $category = [ diff --git a/job-board/app/Models/JobApplication.php b/job-board/app/Models/JobApplication.php index 9e001e8..449aa72 100644 --- a/job-board/app/Models/JobApplication.php +++ b/job-board/app/Models/JobApplication.php @@ -10,7 +10,7 @@ class JobApplication extends Model { use HasFactory; - protected $fillable = ['expected_salary', 'user_id', 'job_id']; + protected $fillable = ['expected_salary', 'user_id', 'job_id', 'cv_path']; public function job(): BelongsTo { diff --git a/job-board/app/Policies/EmployerPolicy.php b/job-board/app/Policies/EmployerPolicy.php new file mode 100644 index 0000000..d496d4c --- /dev/null +++ b/job-board/app/Policies/EmployerPolicy.php @@ -0,0 +1,66 @@ +user_id === $user->id; + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, Employer $employer): bool + { + return false; + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(User $user, Employer $employer): bool + { + return false; + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, Employer $employer): bool + { + return false; + } +} \ No newline at end of file diff --git a/job-board/app/Policies/JobPolicy.php b/job-board/app/Policies/JobPolicy.php index 43f11ad..e803141 100644 --- a/job-board/app/Policies/JobPolicy.php +++ b/job-board/app/Policies/JobPolicy.php @@ -21,7 +21,7 @@ public function viewAny(?User $user): bool */ public function view(?User $user, Job $job): bool { - return true; + return false; } /** @@ -29,15 +29,23 @@ public function view(?User $user, Job $job): bool */ public function create(User $user): bool { - return false; + return $user->employer !== null; } /** * Determine whether the user can update the model. */ - public function update(User $user, Job $job): bool + public function update(User $user, Job $job) { - return false; + if ($job->employer->user_id !== $user->id) { + return false; + } + + if ($job->jobApplications()->count() > 0) { + return Response::deny('Cannot change the job with applications.'); + } + + return true; } /** @@ -45,7 +53,7 @@ public function update(User $user, Job $job): bool */ public function delete(User $user, Job $job): bool { - return false; + return $job->employer->user_id === $user->id; } /** @@ -68,4 +76,9 @@ public function apply(User $user, Job $job): bool { return !$job->hasUserApplied($user); } + + public function downloadCv(User $user, Job $job) + { + return $job->employer->user_id === $user->id; + } } \ No newline at end of file diff --git a/job-board/app/View/Components/Label.php b/job-board/app/View/Components/Label.php new file mode 100644 index 0000000..1ad0e14 --- /dev/null +++ b/job-board/app/View/Components/Label.php @@ -0,0 +1,27 @@ + 'entry', 'senior'] // 0, 1 diff --git a/job-board/config/filesystems.php b/job-board/config/filesystems.php index e9d9dbd..6770850 100644 --- a/job-board/config/filesystems.php +++ b/job-board/config/filesystems.php @@ -39,11 +39,17 @@ 'public' => [ 'driver' => 'local', 'root' => storage_path('app/public'), - 'url' => env('APP_URL').'/storage', + 'url' => env('APP_URL') . '/storage', 'visibility' => 'public', 'throw' => false, ], + 'private' => [ + 'driver' => 'local', + 'root' => storage_path('app/private'), + 'visibility' => 'private', + ], + 's3' => [ 'driver' => 's3', 'key' => env('AWS_ACCESS_KEY_ID'), @@ -73,4 +79,4 @@ public_path('storage') => storage_path('app/public'), ], -]; +]; \ No newline at end of file diff --git a/job-board/database/migrations/2023_06_01_153339_add_cv_to_job_application.php b/job-board/database/migrations/2023_06_01_153339_add_cv_to_job_application.php new file mode 100644 index 0000000..0469b0d --- /dev/null +++ b/job-board/database/migrations/2023_06_01_153339_add_cv_to_job_application.php @@ -0,0 +1,27 @@ +string('cv_path')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('job_applications', function (Blueprint $table) { + $table->dropColumn('cv_path'); + }); + } +}; \ No newline at end of file diff --git a/job-board/database/migrations/2023_06_02_085014_add_soft_deletes_to_jobs_table.php b/job-board/database/migrations/2023_06_02_085014_add_soft_deletes_to_jobs_table.php new file mode 100644 index 0000000..4848fd0 --- /dev/null +++ b/job-board/database/migrations/2023_06_02_085014_add_soft_deletes_to_jobs_table.php @@ -0,0 +1,27 @@ +softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('jobs', function (Blueprint $table) { + $table->dropSoftDeletes(); + }); + } +}; \ No newline at end of file diff --git a/job-board/resources/views/auth/create.blade.php b/job-board/resources/views/auth/create.blade.php index aa0befb..48a32d6 100644 --- a/job-board/resources/views/auth/create.blade.php +++ b/job-board/resources/views/auth/create.blade.php @@ -23,7 +23,7 @@ class="mb-2 block text-sm font-medium text-slate-900">E-mail
-
diff --git a/job-board/resources/views/components/job-card.blade.php b/job-board/resources/views/components/job-card.blade.php index 7c6dcc4..1e9aa3e 100644 --- a/job-board/resources/views/components/job-card.blade.php +++ b/job-board/resources/views/components/job-card.blade.php @@ -7,9 +7,12 @@
-
+
{{ $job->employer->company_name }}
{{ $job->location }}
+ @if ($job->deleted_at) + Deleted + @endif
diff --git a/job-board/resources/views/components/label.blade.php b/job-board/resources/views/components/label.blade.php new file mode 100644 index 0000000..c0720ab --- /dev/null +++ b/job-board/resources/views/components/label.blade.php @@ -0,0 +1,2 @@ + diff --git a/job-board/resources/views/components/layout.blade.php b/job-board/resources/views/components/layout.blade.php index c7a4d6a..c4605f2 100644 --- a/job-board/resources/views/components/layout.blade.php +++ b/job-board/resources/views/components/layout.blade.php @@ -11,7 +11,7 @@ -
@endif + @if (session('error')) + + @endif + {{ $slot }} diff --git a/job-board/resources/views/components/radio-group.blade.php b/job-board/resources/views/components/radio-group.blade.php index 2ba605c..3950ac3 100644 --- a/job-board/resources/views/components/radio-group.blade.php +++ b/job-board/resources/views/components/radio-group.blade.php @@ -1,15 +1,23 @@
- + @if ($allOption) + + @endif @foreach ($optionsWithLabels as $label => $option) @endforeach + + @error($name) +
+ {{ $message }} +
+ @enderror
diff --git a/job-board/resources/views/components/text-input.blade.php b/job-board/resources/views/components/text-input.blade.php index 49ac2dc..07a8539 100644 --- a/job-board/resources/views/components/text-input.blade.php +++ b/job-board/resources/views/components/text-input.blade.php @@ -1,15 +1,36 @@
- @if ($formRef) - + @if ('textarea' != $type) + @if ($formRef) + + @endif + $formRef, + 'ring-slate-300' => !$errors->has($name), + 'ring-red-300' => $errors->has($name), + ]) /> + @else + @endif - + @error($name) +
+ {{ $message }} +
+ @enderror
diff --git a/job-board/resources/views/employer/create.blade.php b/job-board/resources/views/employer/create.blade.php new file mode 100644 index 0000000..2fe29d7 --- /dev/null +++ b/job-board/resources/views/employer/create.blade.php @@ -0,0 +1,13 @@ + + + + @csrf +
+ Company Name + +
+ + Create + +
+
diff --git a/job-board/resources/views/job_application/create.blade.php b/job-board/resources/views/job_application/create.blade.php index 732f9d5..43cd3dc 100644 --- a/job-board/resources/views/job_application/create.blade.php +++ b/job-board/resources/views/job_application/create.blade.php @@ -13,7 +13,8 @@ Your Job Application -
+ @csrf
+
+ + +
+ Apply
diff --git a/job-board/resources/views/my_job/create.blade.php b/job-board/resources/views/my_job/create.blade.php new file mode 100644 index 0000000..5b132e8 --- /dev/null +++ b/job-board/resources/views/my_job/create.blade.php @@ -0,0 +1,48 @@ + + + + +
+ @csrf +
+
+ Job Title + +
+ +
+ Location + +
+ +
+ Salary + +
+ +
+ Description + +
+ +
+ Experience + + +
+
+ Category + + +
+
+ + Create Job +
+
+
diff --git a/job-board/resources/views/my_job/edit.blade.php b/job-board/resources/views/my_job/edit.blade.php new file mode 100644 index 0000000..df9130d --- /dev/null +++ b/job-board/resources/views/my_job/edit.blade.php @@ -0,0 +1,49 @@ + + + + +
+ @csrf + @method('PUT') +
+
+ Job Title + +
+ +
+ Location + +
+ +
+ Salary + +
+ +
+ Description + +
+ +
+ Experience + + +
+
+ Category + + +
+
+ + Update Job +
+
+
diff --git a/job-board/resources/views/my_job/index.blade.php b/job-board/resources/views/my_job/index.blade.php new file mode 100644 index 0000000..76a331a --- /dev/null +++ b/job-board/resources/views/my_job/index.blade.php @@ -0,0 +1,56 @@ + + + +
+ Add New +
+ + @forelse ($jobs as $job) + +

Applications

+
+ @forelse ($job->jobApplications as $application) +
+
+
+ {{ $application->user->name }} +
+
+ Applied {{ $application->created_at->diffForHumans() }} +
+ +
+
+ ${{ number_format($application->expected_salary) }} +
+
+ @empty +
No applications yet
+ @endforelse +
+ +
+ @if (!$job->deleted_at) + Edit +
+ @csrf + @method('DELETE') + Delete +
+ @endif +
+
+ @empty +
+
+ No jobs yet +
+
+ Post your first job here! +
+
+ @endforelse +
diff --git a/job-board/routes/web.php b/job-board/routes/web.php index 7c7e1bf..504188d 100644 --- a/job-board/routes/web.php +++ b/job-board/routes/web.php @@ -1,9 +1,12 @@ only(['index', 'destroy']); + + Route::resource('employer', EmployerController::class); + Route::middleware('employer') + ->resource('my-jobs', MyJobController::class); + + Route::resource('job-application-cvs', MyJobCvController::class)->only('show') + ->parameters(['job-application-cvs' => 'job_application']); }); \ No newline at end of file