Enhance careers management features: update CareersPageController to fetch and display career listings with additional fields, modify Career model to include relationships and new attributes, and create a new view for the careers index with an application modal. Update database migration to reflect changes in the careers table structure and adjust routes for application submissions.

This commit is contained in:
2025-07-29 00:23:08 +05:00
parent 9b3ca3ff66
commit a89e2a71d8
15 changed files with 535 additions and 11 deletions

View File

@@ -0,0 +1,115 @@
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\CareerResource\Pages;
use App\Filament\Resources\CareerResource\RelationManagers;
use App\Models\Career;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
class CareerResource extends Resource
{
protected static ?string $model = Career::class;
protected static ?string $navigationGroup = 'Careers';
protected static ?string $navigationIcon = 'heroicon-o-briefcase';
public static function form(Form $form): Form
{
return $form
->schema([
TextInput::make('title')
->required()
->maxLength(255),
TextInput::make('location')
->required()
->maxLength(255),
Textarea::make('title_description')
->label('Description (show on modal)')
->required()
->maxLength(65535)
->columnSpan('full'),
TextInput::make('salary_per_month')
->required()
->numeric()
->label('Salary per month')
->maxLength(255),
Repeater::make('bullets')
->schema([
TextInput::make('bullet')
->required()
->maxLength(255),
])
->minItems(1)
->defaultItems(1)
->columnSpan('full'),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('title')
->searchable(),
Tables\Columns\TextColumn::make('title_description')
->searchable()
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('salary_per_month')
->searchable()
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('location')
->searchable(),
Tables\Columns\TextColumn::make('salary')
->searchable(),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
RelationManagers\ApplicationsRelationManager::class,
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListCareers::route('/'),
'create' => Pages\CreateCareer::route('/create'),
'edit' => Pages\EditCareer::route('/{record}/edit'),
];
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Filament\Resources\CareerResource\Pages;
use App\Filament\Resources\CareerResource;
use Filament\Actions;
use Filament\Resources\Pages\CreateRecord;
class CreateCareer extends CreateRecord
{
protected static string $resource = CareerResource::class;
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\CareerResource\Pages;
use App\Filament\Resources\CareerResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditCareer extends EditRecord
{
protected static string $resource = CareerResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\CareerResource\Pages;
use App\Filament\Resources\CareerResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListCareers extends ListRecords
{
protected static string $resource = CareerResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace App\Filament\Resources\CareerResource\RelationManagers;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
class ApplicationsRelationManager extends RelationManager
{
protected static string $relationship = 'applications';
public function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('name')
->required()
->maxLength(255),
Forms\Components\DatePicker::make('birthdate')
->required(),
Forms\Components\FileUpload::make('resume_file')
->required()
->acceptedFileTypes(['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'])
->disk('public') // or your preferred disk
->directory('resumes'),
Forms\Components\TextInput::make('email')
->email()
->required()
->maxLength(255),
Forms\Components\TextInput::make('phone_number')
->required()
->maxLength(20),
Forms\Components\Textarea::make('cover_letter')
->maxLength(65535)
->nullable()
->columnSpan('full'),
]);
}
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('name')
->columns([
Tables\Columns\TextColumn::make('name')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('email')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('phone_number')
->searchable(),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->headerActions([
Tables\Actions\CreateAction::make(),
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Application;
class ApplicationController extends Controller
{
public function store(Request $request)
{
$validatedData = $request->validate([
'career_id' => 'required|exists:careers,id',
'name' => 'required|string|max:255',
'birthdate' => 'required|date',
'resume_file' => 'required|file|mimes:pdf,doc,docx|max:2048',
'email' => 'required|email|max:255',
'phone_number' => 'required|string|max:20',
'cover_letter' => 'nullable|string',
]);
$resumePath = $request->file('resume_file')->store('resumes');
Application::create([
'career_id' => $validatedData['career_id'],
'name' => $validatedData['name'],
'birthdate' => $validatedData['birthdate'],
'resume_file' => $resumePath,
'email' => $validatedData['email'],
'phone_number' => $validatedData['phone_number'],
'cover_letter' => $validatedData['cover_letter'] ?? null,
]);
return back()->with('success', 'Your application has been submitted successfully!');
}
}

View File

@@ -9,12 +9,9 @@ class CareersPageController extends Controller
{
public function index()
{
return view('web.pages.careers.index');
}
$careers = Career::query()->get();
public function store(Request $request)
{
dd($request->all());
return view('web.pages.careers.index', compact('careers'));
}
public function show(Career $career)

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use App\Models\Career;
class Application extends Model
{
protected $fillable = [
'career_id',
'name',
'birthdate',
'resume_file',
'email',
'phone_number',
'cover_letter',
];
public function career(): BelongsTo
{
return $this->belongsTo(Career::class);
}
}

View File

@@ -3,13 +3,25 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use App\Models\Application;
class Career extends Model
{
protected $fillable = [
'title',
'description',
'title_description',
'salary_per_month',
'bullets',
'location',
'salary',
];
protected $casts = [
'bullets' => 'array',
];
public function applications(): HasMany
{
return $this->hasMany(Application::class);
}
}

View File

@@ -55,6 +55,10 @@
"@php artisan migrate --graceful --ansi"
],
"dev": [
"Composer\\Config::disableProcessTimeout",
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite"
],
"network": [
"Composer\\Config::disableProcessTimeout",
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve --host=0.0.0.0\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite"
],

View File

@@ -14,9 +14,10 @@ return new class extends Migration
Schema::create('careers', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('description');
$table->text('title_description')->nullable();
$table->string('salary_per_month')->nullable();
$table->json('bullets')->nullable();
$table->string('location');
$table->string('salary')->nullable();
$table->timestamps();
});
}

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('applications', function (Blueprint $table) {
$table->id();
$table->foreignId('career_id')->constrained()->onDelete('cascade');
$table->string('name');
$table->date('birthdate');
$table->string('resume_file');
$table->string('email');
$table->string('phone_number');
$table->text('cover_letter')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('applications');
}
};

View File

@@ -5538,7 +5538,7 @@ p {
}
.price__area-item.active .price__area-item-price h3,
.price__area-item.active .price__area-item-price h2 {
color: var(--color-1);
color: var(--text-white);
}
.price__area-item.active .price__area-item-list {
border-color: #DDB348;

View File

@@ -0,0 +1,170 @@
@extends('web.layouts.app')
@push('header-css')
<style>
#applicationForm > label > span {
color: var(--gujurly-primary);
}
</style>
@endpush
@section('content')
<!-- Breadcrumb Area Start -->
<div class="breadcrumb__area" style="background-image: url('/web/assets/img/page/breadcrumb.jpg');">
<div class="container">
<div class="row">
<div class="col-xl-12">
<div class="breadcrumb__area-content">
<h2>Careers</h2>
<ul>
<li><a href="/">Home</a><i class="fa-regular fa-angle-right"></i></li>
<li>Careers</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- Breadcrumb Area End -->
<!-- Pricing Plan Area Start -->
<div class="price__area section-padding">
<div class="container">
<div class="row">
<div class="col-xl-4 col-md-6 xl-mb-25 wow fadeInUp" data-wow-delay=".4s">
@forelse ($careers as $career)
<div class="price__area-item">
<div class="price__area-item-price">
<span>{{ $career->title }}</span>
<h3>{{ $career->location }}</h3>
<h2>{{ $career->salary_per_month }}<span>/Per monthly</span></h2>
</div>
<div class="price__area-item-list">
<ul>
@foreach($career->bullets as $bullet)
<li><i class="flaticon-checked"></i>{{ $bullet['bullet'] ?? '' }}</li>
@endforeach
</ul>
</div>
<div class="price__area-item-btn">
<button type="button" class="build_button apply-now-button" data-bs-toggle="modal" data-bs-target="#applicationModal" data-career-title="{{ $career->title }}" data-career-location="{{ $career->location }}" data-career-salary="{{ $career->salary_per_month }}" data-career-description="{{ $career->title_description }}" data-career-id="{{ $career->id }}">Apply Now<i class="flaticon-right-up"></i></button>
</div>
</div>
@empty
<span>No careers found...</span>
@endforelse
</div>
</div>
</div>
</div>
<!-- Pricing Plan Area End -->
<!-- Application Modal -->
<div class="modal fade" id="applicationModal" tabindex="-1" aria-labelledby="applicationModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content rounded-3">
<div class="modal-header">
<h5 class="modal-title" id="applicationModalLabel">Apply for <span id="jobTitle"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<h6 id="jobLocation"></h6>
<p id="jobSalary"></p>
<p id="jobDescription"></p>
<form id="applicationForm" enctype="multipart/form-data" action="{{ route('applications.store') }}" method="POST">
@csrf
<input type="hidden" name="career_id" id="careerId">
<div class="row">
<div class="col-md-6 mb-3">
<label for="name" class="form-label">Name <span> *</span></label>
<input type="text" class="form-control" id="name" name="name" required>
</div>
<div class="col-md-6 mb-3">
<label for="birthdate" class="form-label">Birthdate</label>
<input type="date" class="form-control" id="birthdate" name="birthdate" required>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="col-md-6 mb-3">
<label for="phone_number" class="form-label">Phone Number</label>
<input type="text" class="form-control" id="phone_number" name="phone_number" required>
</div>
</div>
<div class="mb-3">
<label for="resume_file" class="form-label">Resume (PDF, DOC, DOCX)</label>
<input type="file" class="form-control" id="resume_file" name="resume_file" accept=".pdf,.doc,.docx" required>
</div>
<div class="mb-3">
<label for="cover_letter" class="form-label">Cover Letter (Optional)</label>
<textarea class="form-control" id="cover_letter" name="cover_letter" rows="5"></textarea>
</div>
<button type="submit" class="btn btn-primary">Submit Application</button>
</form>
</div>
</div>
</div>
</div>
@endsection
@push('footer-js')
<script>
document.addEventListener('DOMContentLoaded', function () {
var applicationModal = document.getElementById('applicationModal');
applicationModal.addEventListener('show.bs.modal', function (event) {
var button = event.relatedTarget;
var careerTitle = button.getAttribute('data-career-title');
var careerLocation = button.getAttribute('data-career-location');
var careerSalary = button.getAttribute('data-career-salary');
var careerDescription = button.getAttribute('data-career-description');
var careerId = button.getAttribute('data-career-id');
var modalTitle = applicationModal.querySelector('#jobTitle');
var modalLocation = applicationModal.querySelector('#jobLocation');
var modalSalary = applicationModal.querySelector('#jobSalary');
var modalDescription = applicationModal.querySelector('#jobDescription');
var modalCareerId = applicationModal.querySelector('#careerId');
modalTitle.textContent = careerTitle;
modalLocation.textContent = 'Location: ' + careerLocation;
modalSalary.textContent = 'Salary: ' + careerSalary + ' / Per monthly';
modalDescription.textContent = careerDescription;
modalCareerId.value = careerId;
});
// Handle form submission with AJAX
document.getElementById('applicationForm').addEventListener('submit', function (e) {
e.preventDefault();
let form = e.target;
let formData = new FormData(form);
fetch(form.action, {
method: 'POST',
body: formData,
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert(data.message);
var modal = bootstrap.Modal.getInstance(applicationModal);
modal.hide();
form.reset();
} else {
alert('Error: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred. Please try again.');
});
});
});
</script>
@endpush

View File

@@ -10,6 +10,7 @@ use App\Http\Controllers\NewsPageController;
use App\Http\Controllers\OurSolutionPageController;
use App\Http\Controllers\StoryPageController;
use App\Http\Controllers\Web\SuccessPageController;
use App\Http\Controllers\ApplicationController;
use Illuminate\Support\Facades\Route;
// Homepage...
@@ -33,8 +34,8 @@ Route::get('success-stories/{success:slug}', [SuccessPageController::class, 'sho
// Careers...
Route::get('careers', [CareersPageController::class, 'index'])->name('career.index');
Route::get('careers/{career:slug}', [CareersPageController::class, 'show'])->name('career.show');
Route::post('careers', [CareersPageController::class, 'store'])->name('career.store');
Route::post('applications', [ApplicationController::class, 'store'])->name('applications.store');
// Internships...
Route::get('internships', [InternshipsPageController::class, 'index'])->name('internship.index');