This commit is contained in:
Mark Gregory
2026-07-24 10:47:01 +01:00
parent dbb9566c49
commit 3f87bea5f9
62 changed files with 402 additions and 5 deletions
+2
View File
@@ -0,0 +1,2 @@
<?php
return ['name' => 'Careers'];
@@ -0,0 +1,3 @@
<?php
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema;
return new class extends Migration { public function up(): void { Schema::create('career_jobs',function(Blueprint $t): void{$t->id();$t->string('title');$t->string('slug')->unique();$t->string('department')->nullable();$t->string('location')->nullable();$t->string('employment_type')->default('full-time');$t->longText('description')->nullable();$t->string('status')->default('draft')->index();$t->timestamp('published_at')->nullable();$t->timestamp('closes_at')->nullable();$t->timestamps();}); Schema::create('career_applications',function(Blueprint $t): void{$t->id();$t->foreignId('job_id')->constrained('career_jobs')->cascadeOnDelete();$t->string('name');$t->string('email')->index();$t->string('phone')->nullable();$t->string('cv_path')->nullable();$t->text('cover_letter')->nullable();$t->string('status')->default('new')->index();$t->text('notes')->nullable();$t->timestamps();}); } public function down(): void { Schema::dropIfExists('career_applications');Schema::dropIfExists('career_jobs'); } };
@@ -0,0 +1,3 @@
<?php
namespace Modules\Careers\Http\Controllers; use App\Http\Controllers\Controller; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\View\View; use Modules\Careers\Models\Job; use Modules\Careers\Models\Application;
final class CareerController extends Controller { public function index(): View { return view('careers::admin.index',['jobs'=>Job::withCount('applications')->latest()->paginate(20),'applications'=>Application::latest()->limit(10)->get()]); } public function store(Request $r): RedirectResponse { Job::create($r->validate(['title'=>['required','string','max:255'],'slug'=>['required','string','max:255','unique:career_jobs,slug'],'department'=>['nullable','string','max:120'],'location'=>['nullable','string','max:120'],'employment_type'=>['required','in:full-time,part-time,contract,temporary,internship'],'description'=>['nullable','string'],'status'=>['required','in:draft,published,closed'],'published_at'=>['nullable','date'],'closes_at'=>['nullable','date','after:published_at']])); return back()->with('status','Job created.'); } public function destroy(Job $job): RedirectResponse { $job->delete(); return back()->with('status','Job deleted.'); } }
+3
View File
@@ -0,0 +1,3 @@
<?php
namespace Modules\Careers\Models; use Illuminate\Database\Eloquent\Model;
final class Application extends Model { protected $table='career_applications'; protected $fillable=['job_id','name','email','phone','cv_path','cover_letter','status','notes']; }
+3
View File
@@ -0,0 +1,3 @@
<?php
namespace Modules\Careers\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany;
final class Job extends Model { protected $table='career_jobs'; protected $fillable=['title','slug','department','location','employment_type','description','status','published_at','closes_at']; protected function casts(): array { return ['published_at'=>'datetime','closes_at'=>'datetime']; } public function applications(): HasMany { return $this->hasMany(Application::class); } }
@@ -0,0 +1,16 @@
<?php
namespace Modules\Careers\Providers;
use Illuminate\Support\ServiceProvider;
final class CareersServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->register(RouteServiceProvider::class);
$this->mergeConfigFrom(module_path('Careers', 'Config/config.php'), 'careers');
}
public function boot(): void
{
$this->loadMigrationsFrom(module_path('Careers', 'Database/Migrations'));
$this->loadViewsFrom(module_path('Careers', 'Resources/views'), 'careers');
}
}
@@ -0,0 +1,12 @@
<?php
namespace Modules\Careers\Providers;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;
final class RouteServiceProvider extends ServiceProvider
{
public function boot(): void
{
parent::boot();
Route::middleware('web')->group(module_path('Careers', 'Routes/web.php'));
}
}
@@ -0,0 +1 @@
<x-layouts.app :title="__('Careers')"><div class="space-y-8"><div><h1 class="text-2xl font-semibold">Careers</h1><p class="text-sm text-zinc-500">Manage vacancies and candidate applications.</p></div>@if(session('status'))<div class="rounded border border-green-300 bg-green-50 p-3 text-green-800">{{ session('status') }}</div>@endif<form method="POST" action="{{ route('administration.careers.store') }}" class="space-y-4 rounded-xl border p-5">@csrf<h2 class="font-semibold">Create vacancy</h2><div class="grid gap-4 md:grid-cols-2"><flux:input name="title" label="Job title" required/><flux:input name="slug" label="Slug" required/><flux:input name="department" label="Department"/><flux:input name="location" label="Location"/><flux:select name="employment_type" label="Employment type"><option value="full-time">Full time</option><option value="part-time">Part time</option><option value="contract">Contract</option><option value="temporary">Temporary</option><option value="internship">Internship</option></flux:select><flux:select name="status" label="Status"><option value="draft">Draft</option><option value="published">Published</option><option value="closed">Closed</option></flux:select></div><flux:textarea name="description" label="Description" rows="8"/><flux:button type="submit" variant="primary">Create vacancy</flux:button></form><div class="overflow-hidden rounded-xl border"><table class="w-full text-sm"><thead class="bg-zinc-50"><tr><th class="p-3 text-left">Vacancy</th><th class="p-3 text-left">Department</th><th class="p-3 text-left">Status</th><th class="p-3 text-left">Applications</th><th class="p-3"></th></tr></thead><tbody>@forelse($jobs as $job)<tr class="border-t"><td class="p-3 font-medium">{{ $job->title }}</td><td class="p-3">{{ $job->department ?: '—' }}</td><td class="p-3">{{ ucfirst($job->status) }}</td><td class="p-3">{{ $job->applications_count }}</td><td class="p-3 text-right"><form method="POST" action="{{ route('administration.careers.destroy',$job) }}">@csrf @method('DELETE')<flux:button size="sm" variant="danger" type="submit">Delete</flux:button></form></td></tr>@empty<tr><td colspan="5" class="p-6 text-center text-zinc-500">No vacancies yet.</td></tr>@endforelse</tbody></table></div>{{ $jobs->links() }}</div></x-layouts.app>
+3
View File
@@ -0,0 +1,3 @@
<?php
use Illuminate\Support\Facades\Route; use Modules\Careers\Http\Controllers\CareerController;
Route::middleware(['auth','verified'])->prefix('administration/careers')->name('administration.careers.')->group(function(): void { Route::get('/',[CareerController::class,'index'])->name('index'); Route::post('/',[CareerController::class,'store'])->name('store'); Route::delete('/{job}',[CareerController::class,'destroy'])->name('destroy'); });
@@ -0,0 +1,10 @@
<?php
namespace Modules\Careers\Tests\Feature;
use Tests\TestCase;
final class ModuleBootTest extends TestCase
{
public function test_module_configuration_is_available(): void
{
$this->assertSame('Careers', config('careers.name'));
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"name": "Careers",
"alias": "careers",
"version": "1.0.0",
"description": "Job vacancies and candidate application workflow.",
"keywords": ["business", "careers"],
"priority": 43,
"providers": ["Modules\\Careers\\Providers\\CareersServiceProvider"],
"aliases": {},
"files": [],
"requires": ["Core", "Administration"]
}