feat: Implement Career Management Module

- Created CareerController for handling career-related CRUD operations.
- Added Career model with necessary attributes and relationships.
- Created migration for careers table with relevant fields.
- Developed views for creating, editing, and listing careers.
- Implemented DataTables for career listing with action buttons.
- Added routes for career management and integrated with sidebar.
- Created client-side career detail template and updated career listing page.
- Added helper functions to fetch active careers for display.
This commit is contained in:
2025-08-22 13:52:06 +05:45
parent a11de7be9e
commit 52732b0f09
16 changed files with 787 additions and 159 deletions

View File

@@ -0,0 +1,137 @@
<?php
namespace Modules\CCMS\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Modules\CCMS\Models\Career;
use Yajra\DataTables\Facades\DataTables;
class CareerController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
if (request()->ajax()) {
$model = Career::query()->orderBy('order');
return DataTables::eloquent($model)
->addIndexColumn()
->setRowClass('tableRow')
->editColumn('status', function (Career $career) {
$status = $career->status ? 'Published' : 'Draft';
$color = $career->status ? 'text-success' : 'text-danger';
return "<p class='{$color}'>{$status}</p>";
})
->addColumn('action', 'ccms::career.datatable.action')
->rawColumns(['status', 'action'])
->toJson();
}
return view('ccms::career.index', [
'title' => 'Career List',
]);
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
$careerOptions = Career::where('status', 1)->pluck('job_title', 'id');
return view('ccms::career.create', [
'title' => 'Create Career',
'editable' => false,
'careerOptions' => $careerOptions
]);
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
$maxOrder = Career::max('order');
$order = $maxOrder ? ++$maxOrder : 1;
$request->mergeIfMissing([
'slug' => Str::slug($request->title),
// 'order' => $order,
]);
Career::create($request->all());
flash()->success("Career has been created!");
return redirect()->route('career.index');
}
/**
* Show the specified resource.
*/
public function show($id)
{
return view('ccms::show');
}
/**
* Show the form for editing the specified resource.
*/
public function edit($id)
{
$careerOptions = Career::where('status', 1)->pluck('job_title', 'id');
$career = Career::findOrFail($id);
return view('ccms::career.edit', [
'title' => 'Edit Career',
'editable' => true,
'career' => $career,
'careerOptions' => $careerOptions
]);
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, $id)
{
$request->merge([
'slug' => Str::slug($request->title),
]);
$validated = $request->validate([]);
$career = Career::findOrFail($id);
$career->update($request->all());
flash()->success("Career has been updated.");
return redirect()->back();
}
/**
* Remove the specified resource from storage.
*/
public function destroy($id)
{
$career = Career::findOrFail($id);
$career->delete();
return response()->json(['status' => 200, 'message' => "Career has been deleted."], 200);
}
public function reorder(Request $request)
{
$careers = Career::all();
foreach ($careers as $career) {
foreach ($request->order as $order) {
if ($order['id'] == $career->id) {
$career->update(['order' => $order['position']]);
}
}
}
return response(['status' => true, 'message' => 'Reordered successfully'], 200);
}
public function toggle($id)
{
$career = Career::findOrFail($id);
$career->update(['status' => !$career->status]);
return response(['status' => 200, 'message' => 'Toggled successfully'], 200);
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace Modules\CCMS\Models;
use App\Traits\CreatedUpdatedBy;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Modules\CCMS\Traits\UpdateCustomFields;
use Modules\Document\Models\Document;
use App\Traits\AddToDocumentCollection;
class Career extends Model
{
use HasFactory, UpdateCustomFields, AddToDocumentCollection, CreatedUpdatedBy;
protected $fillable = [
'department',
'job_title',
'job_description',
'job_requirements',
'salary_range',
'location',
'position',
'start_date',
'end_date',
'status',
'createdby',
'updatedby',
'order',
];
protected function casts(): array
{
return [
'custom' => 'array',
];
}
protected function images(): Attribute
{
return Attribute::make(
get: function ($value) {
if (empty($value)) {
return [];
}
$parts = explode(',', $value);
return array_map(fn($part) => asset(trim($part)), $parts);
}
);
}
protected function image(): Attribute
{
return Attribute::make(
get: fn($value) => asset($value),
);
}
protected function banner(): Attribute
{
return Attribute::make(
get: fn($value) => asset($value),
);
}
protected function sidebarImage(): Attribute
{
return Attribute::make(
get: fn($value) => asset($value),
);
}
protected function iconImage(): Attribute
{
return Attribute::make(
get: fn($value) => asset($value),
);
}
public function children()
{
return $this->hasMany(Career::class, 'parent_id');
}
public function parent()
{
return $this->belongsTo(Career::class, 'parent_id');
}
public function documents()
{
return $this->morphMany(Document::class, 'documentable');
}
}

View File

@@ -0,0 +1,40 @@
<?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('careers', function (Blueprint $table) {
$table->id();
$table->string('department')->nullable();
$table->string('job_title')->nullable();
$table->text('job_description')->nullable();
$table->text('job_requirements')->nullable();
$table->string('salary_range')->nullable();
$table->string('location')->nullable();
$table->string('position')->nullable();
$table->date('start_date')->nullable();
$table->date('end_date')->nullable();
$table->integer('status')->default(1);
$table->integer('createdby')->unsigned()->nullable();
$table->integer('updatedby')->unsigned()->nullable();
$table->integer('order')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('careers');
}
};

View File

@@ -0,0 +1,16 @@
@extends('layouts.app')
@section('content')
<x-dashboard.breadcumb :title="$title" />
<div class="container-fluid">
@if ($errors->any())
<x-flash-message type="danger" :messages="$errors->all()" />
@endif
<div class="row">
<div class="col-xl-12">
{{ html()->form('POST')->route('career.store')->class('needs-validation')->attributes(['novalidate'])->open() }}
@include('ccms::career.partials._form')
{{ html()->form()->close() }}
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,12 @@
<div class="hstack flex-wrap gap-3">
<a href="{{ route('career.edit', $id) }}" data-bs-toggle="tooltip"
data-bs-placement="bottom" data-bs-title="Edit" class="link-success fs-15 edit-item-btn"><i
class=" ri-edit-2-line"></i></a>
<a data-link="{{ route('career.toggle', $id) }}" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="Toggle" data-status="{{ $status == 1 ? 'Draft' : 'Published' }}"
class="link-info fs-15 toggle-item"><i class="{{ $status == 1 ? 'ri-toggle-line' : 'ri-toggle-fill' }}"></i></a>
<a href="javascript:void(0);" data-link="{{ route('career.destroy', $id) }}" class="link-danger fs-15 remove-item"
data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="Delete"><i class="ri-delete-bin-6-line"></i>
</a>
</div>

View File

@@ -0,0 +1,16 @@
@extends('layouts.app')
@section('content')
<x-dashboard.breadcumb :title="$title" />
<div class="container-fluid">
@if ($errors->any())
<x-flash-message type="danger" :messages="$errors->all()" />
@endif
<div class="row">
<div class="col-xl-12">
{{ html()->modelForm($career, 'PUT')->route('career.update', $career->id)->class('needs-validation')->attributes(['novalidate'])->open() }}
@include('ccms::career.partials._form')
{{ html()->closeModelForm() }}
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,51 @@
@extends('layouts.app')
@section('content')
<div class="container-fluid">
<x-dashboard.breadcumb :title="$title" />
@if ($errors->any())
<x-flash-message type="danger" :messages="$errors->all()" />
@endif
<div class="row">
<div class="col-xl-12">
<div class="card">
<div class="card-header d-flex align-items-center justify-content-between">
<h5 class="card-title mb-0">{{ $title }}</h5>
<a href="{{ route('career.create') }}" class="btn btn-primary waves-effect waves-light text-white"><i
class="ri-add-line align-middle"></i> Create</a>
</div>
<div class="card-body">
@php
$columns = [
[
'title' => 'S.N',
'data' => 'DT_RowIndex',
'name' => 'DT_RowIndex',
'orderable' => false,
'searchable' => false,
'sortable' => false,
],
['title' => 'Job Title', 'data' => 'job_title', 'name' => 'job_title'],
['title' => 'Start Date', 'data' => 'start_date', 'name' => 'start_date'],
['title' => 'End Date', 'data' => 'end_date', 'name' => 'end_date'],
['title' => 'Department', 'data' => 'department', 'name' => 'department'],
['title' => 'Location', 'data' => 'location', 'name' => 'location'],
['title' => 'Position', 'data' => 'position', 'name' => 'position'],
['title' => 'Salary', 'data' => 'salary_range', 'name' => 'salary_range'],
['title' => 'Status', 'data' => 'status', 'name' => 'status'],
[
'title' => 'Action',
'data' => 'action',
'orderable' => false,
'searchable' => false,
],
];
@endphp
<x-data-table-script :route="route('career.index')" :reorder="route('career.reorder')" :columns="$columns" />
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,220 @@
<div class="row">
<div class="col-xl-8">
<div class="card h-auto">
<div class="card-body">
<div class="row gy-3">
<div class="col-md-6">
{{ html()->label('Job Title')->class('form-label')->for('job_title') }}
{{ html()->span('*')->class('text-danger') }}
{{ html()->text('job_title')->class('form-control')->placeholder('Enter Job Title')->required(true) }}
</div>
<div class="col-md-6">
{{ html()->label('Department')->class('form-label')->for('department') }}
{{ html()->span('*')->class('text-danger') }}
{{ html()->text('department')->class('form-control')->placeholder('Enter Department')->required(true) }}
</div>
<div class="col-md-6">
{{ html()->label('Vacancy Start Date')->class('form-label') }}
<div class="input-group">
{{ html()->text('start_date')->class('form-control')->id('career-start-date')->placeholder('Vacancy Start Date')->attributes([
'data-provider' => 'flatpickr',
'data-date-format' => 'Y-m-d',
'data-enable-time' => '',
])->required() }}
<span class="input-group-text"><i class="ri-calendar-career-line"></i></span>
</div>
</div>
<div class="col-md-6">
{{ html()->label('Vacancy End Date')->class('form-label') }}
<div class="input-group">
{{ html()->text('end_date')->class('form-control')->id('career-end-date')->placeholder('Vacancy End Date')->attributes([
'data-provider' => 'flatpickr',
'data-date-format' => 'Y-m-d',
'data-enable-time' => '',
]) }}
<span class="input-group-text"><i class="ri-calendar-career-line"></i></span>
</div>
</div>
<div class="col-md-6">
{{ html()->label('Salary Range')->class('form-label')->for('salary_range') }}
{{ html()->span('*')->class('text-danger') }}
{{ html()->text('salary_range')->class('form-control')->placeholder('Enter Salary Range')->required(true) }}
</div>
<div class="col-md-6">
{{ html()->label('Location')->class('form-label')->for('location') }}
{{ html()->span('*')->class('text-danger') }}
{{ html()->text('location')->class('form-control')->placeholder('Enter location')->required(true) }}
</div>
<div class="col-12">
{{ html()->label('Position')->class('form-label')->for('position') }}
{{ html()->span('*')->class('text-danger') }}
{{ html()->text('position')->class('form-control')->placeholder('Enter Position (e.g. Fresher, Intermidiate, Senior)')->required(true) }}
</div>
<div class="col-12">
{{ html()->label('Job Description')->class('form-label')->for('job_description') }}
{{ html()->textarea('job_description')->class('form-control')->placeholder('Enter Job Description (JD)')->rows(5) }}
</div>
<div class="col-12">
{{ html()->label('Job Requirements')->class('form-label')->for('job_requirements') }}
{{ html()->textarea('job_requirements')->class('form-control ckeditor-classic')->placeholder('Enter Job Requirements') }}
</div>
</div>
</div>
</div>
{{-- <x-ccms::custom-form-field :data="$career->custom ?? []" /> --}}
<div class="card meta-section">
<div class="card-header">
<h6 class="card-title mb-0 fs-14">Meta</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-xl-12 col-sm-12">
{{ html()->label('Meta Title')->class('form-label')->for('meta_title') }}
{{ html()->text('meta_title')->class('form-control mb-3')->placeholder('Meta Title') }}
</div>
<div class="col-xl-12 col-sm-12">
{{ html()->label('Meta Keywords')->class('form-label')->for('meta_keywords') }}
{{ html()->textarea('meta_keywords')->class('form-control mb-3')->placeholder('Meta Keywords') }}
</div>
<div class="col-xl-12 col-sm-12">
{{ html()->label('Meta Description')->class('form-label')->for('meta_description') }}
{{ html()->textarea('meta_description')->class('form-control mb-3')->placeholder('Meta wire:Description')->rows(3) }}
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-4">
<div class="card">
<div class="card-header">
<h6 class="card-title mb-0 fs-14">
Published
</h6>
</div>
<div class="card-body">
{{ html()->label('Status')->class('form-label visually-hidden')->for('status') }}
{{ html()->select('status', config('constants.page_status_options'))->class('form-select choices-select') }}
</div>
<x-form-buttons :href="route('career.index')" :label="isset($career) ? 'Update' : 'Create'" />
</div>
<div class="card">
<div class="card-header">
<h6 class="card-title mb-0 fs-14">
Page Attributes
</h6>
</div>
<div class="card-body">
{{ html()->label('Parent Event')->class('form-label')->for('parent_id') }}
{{ html()->select('parent_id', $careerOptions ?? [])->value($career->parent_id ?? old('parent_id'))->class('form-select choices-select')->placeholder('Select') }}
</div>
</div>
<div class="card media-gallery-section">
<div class="card-header">
<h6 class="card-title mb-0 fs-14">
Icon
</h6>
</div>
<div class="card-body">
<div class="mb-3">
{{ html()->label('Icon')->class('form-label')->for('icon_class') }}
{{ html()->text('icon_class')->class('form-control')->placeholder('Icon class') }}
</div>
{{ html()->label('Icon Image')->class('form-label')->for('icon_image') }}
<x-image-input :data="$editable ? $career->getRawOriginal('icon_image') : null" id="icon_image" name="icon_image" :editable="$editable" :multiple=false />
</div>
</div>
<div class="card featured-image-section">
<div class="card-header">
<h6 class="card-title mb-0 fs-14">
Featured Image
</h6>
</div>
<div class="card-body">
<div class="mb-3">
{{ html()->label('Featured')->class('form-label')->for('image') }}
<x-image-input :data="$editable ? $career->getRawOriginal('image') : null" id="image" name="image" :editable="$editable" :multiple=false />
</div>
{{ html()->label('Banner')->class('form-label')->for('banner') }}
<x-image-input :data="$editable ? $career->getRawOriginal('banner') : null" id="banner" name="banner" :editable="$editable" :multiple=false />
</div>
</div>
<div class="card media-gallery-section">
<div class="card-header">
<h6 class="card-title mb-0 fs-14">
Media Gallery
</h6>
</div>
<div class="card-body">
<x-image-input :editable="$editable" id="images" name="images" :data="$editable ? $career->getRawOriginal('images') : null" :multiple="true"
label="Select Images" />
</div>
</div>
<div class="card sidebar-section">
<div class="card-header d-flex jusitfy-content-between align-items-center">
<h6 class="card-title mb-0 fs-14">Sidebar</h6>
</div>
<div class="card-body">
<div class="row gy-3">
<div class="col-lg-12">
{{ html()->label('Title')->class('form-label')->for('sidebar_title') }}
{{ html()->text('sidebar_title')->class('form-control') }}
</div>
<div class="col-lg-12">
{{ html()->label('Content')->class('form-label')->for('sidebar_content') }}
{{ html()->textarea('sidebar_content')->class('form-control')->placeholder('Short Content (optional)')->rows(3) }}
</div>
<div class="col-lg-12">
{{ html()->label('Image')->class('form-label')->for('sidebar_content') }}
<x-image-input :data="$editable ? $career->getRawOriginal('sidebar_image') : null" id="sidebar_image" name="sidebar_image" :editable="$editable"
:multiple=false />
</div>
</div>
</div>
</div>
<div class="card button-section">
<div class="card-header d-flex jusitfy-content-between align-items-center">
<h6 class="card-title mb-0 fs-14">Button</h6>
</div>
<div class="card-body">
<div class="row gy-3">
<div class="col-lg-12">
{{ html()->label('Text')->class('form-label')->for('button_text') }}
{{ html()->text('button_text')->class('form-control') }}
</div>
<div class="col-lg-12">
{{ html()->label('Link')->class('form-label')->for('button_url') }}
{{ html()->text('button_url')->class('form-control')->placeholder('Button Link') }}
</div>
<div class="col-lg-12">
{{ html()->label('Target')->class('form-label')->for('button_target') }}
{{ html()->select('button_target', config('constants.redirect_options'))->class('form-select choices-select') }}
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -27,7 +27,6 @@
'sortable' => false,
],
['title' => 'Image', 'data' => 'image', 'name' => 'image'],
['title' => 'Parent', 'data' => 'parent_id', 'name' => 'parent_ids'],
['title' => 'Title', 'data' => 'title', 'name' => 'title'],
['title' => 'Start Date', 'data' => 'start_date', 'name' => 'start_date'],
['title' => 'End Date', 'data' => 'end_date', 'name' => 'end_date'],

View File

@@ -3,6 +3,7 @@
use Illuminate\Support\Facades\Route;
use Modules\CCMS\Http\Controllers\BlogController;
use Modules\CCMS\Http\Controllers\BranchController;
use Modules\CCMS\Http\Controllers\CareerController;
use Modules\CCMS\Http\Controllers\CategoryController;
use Modules\CCMS\Http\Controllers\CounselorController;
use Modules\CCMS\Http\Controllers\CounterController;
@@ -90,6 +91,10 @@ Route::group(['middleware' => ['web', 'auth', 'permission'], 'prefix' => 'admin/
Route::get('event/toggle/{id}', [EventController::class, 'toggle'])->name('event.toggle');
Route::resource('event', EventController::class)->names('event');
Route::post('career/reorder', [CareerController::class, 'reorder'])->name('career.reorder');
Route::get('career/toggle/{id}', [CareerController::class, 'toggle'])->name('career.toggle');
Route::resource('career', CareerController::class)->names('career');
Route::post('branch/reorder', [BranchController::class, 'reorder'])->name('branch.reorder');
Route::get('branch/toggle/{id}', [BranchController::class, 'toggle'])->name('branch.toggle');
Route::resource('branch', BranchController::class)->names('branch');

View File

@@ -25,6 +25,8 @@ Route::post('enquiry', [EnquiryController::class, 'store'])->name('enquiry.store
Route::post('franchise', [FranchiseController::class, 'store'])->name('franchise.store');
Route::post('newsletter', [NewsletterController::class, 'store'])->name('newsletter.store');
Route::get('career/{id}', [WebsiteController::class, 'careerSingle'])->name('career.single');
Route::get('getCost', [WebsiteController::class, 'getCost'])->name('cost.getCost');
Route::get('/thankyou', [WebsiteController::class, 'thankyouPage'])->name('thankyou');