Skip to content

Commit 322017b

Browse files
committed
feat(hr): Phase 132 — HR Competency Frameworks
Add CompetencyFramework and Competency models with migrations, policy, controller (resource + activate/archive actions), routes, React stubs, and feature tests (9 passing). https://claude.ai/code/session_01RdUGwo74JXChRCF88Yu27d
1 parent 376124a commit 322017b

13 files changed

Lines changed: 801 additions & 0 deletions

File tree

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<?php
2+
3+
namespace App\Modules\HR\Http\Controllers;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Modules\HR\Models\CompetencyFramework;
7+
use Illuminate\Http\RedirectResponse;
8+
use Illuminate\Http\Request;
9+
use Inertia\Inertia;
10+
use Inertia\Response;
11+
12+
class CompetencyFrameworkController extends Controller
13+
{
14+
public function index(): Response
15+
{
16+
$this->authorize('viewAny', CompetencyFramework::class);
17+
18+
$frameworks = CompetencyFramework::with('competencies')
19+
->latest()
20+
->paginate(25)
21+
->withQueryString();
22+
23+
return Inertia::render('HR/CompetencyFrameworks/Index', [
24+
'frameworks' => $frameworks,
25+
]);
26+
}
27+
28+
public function create(): Response
29+
{
30+
$this->authorize('create', CompetencyFramework::class);
31+
32+
return Inertia::render('HR/CompetencyFrameworks/Create');
33+
}
34+
35+
public function store(Request $request): RedirectResponse
36+
{
37+
$this->authorize('create', CompetencyFramework::class);
38+
39+
$validated = $request->validate([
40+
'name' => ['required', 'string', 'max:255'],
41+
'description' => ['nullable', 'string'],
42+
]);
43+
44+
CompetencyFramework::create([
45+
...$validated,
46+
'tenant_id' => app('tenant')->id,
47+
'created_by' => auth()->id(),
48+
]);
49+
50+
return redirect()->route('hr.competency-frameworks.index')
51+
->with('success', 'Competency framework created.');
52+
}
53+
54+
public function show(CompetencyFramework $competencyFramework): Response
55+
{
56+
$this->authorize('view', $competencyFramework);
57+
58+
$competencyFramework->load('competencies');
59+
60+
return Inertia::render('HR/CompetencyFrameworks/Show', [
61+
'framework' => $competencyFramework,
62+
]);
63+
}
64+
65+
public function edit(CompetencyFramework $competencyFramework): Response
66+
{
67+
$this->authorize('update', $competencyFramework);
68+
69+
return Inertia::render('HR/CompetencyFrameworks/Edit', [
70+
'framework' => $competencyFramework,
71+
]);
72+
}
73+
74+
public function update(Request $request, CompetencyFramework $competencyFramework): RedirectResponse
75+
{
76+
$this->authorize('update', $competencyFramework);
77+
78+
$validated = $request->validate([
79+
'name' => ['required', 'string', 'max:255'],
80+
'description' => ['nullable', 'string'],
81+
]);
82+
83+
$competencyFramework->update($validated);
84+
85+
return redirect()->route('hr.competency-frameworks.index')
86+
->with('success', 'Competency framework updated.');
87+
}
88+
89+
public function destroy(CompetencyFramework $competencyFramework): RedirectResponse
90+
{
91+
$this->authorize('delete', $competencyFramework);
92+
93+
$competencyFramework->delete();
94+
95+
return redirect()->route('hr.competency-frameworks.index')
96+
->with('success', 'Competency framework deleted.');
97+
}
98+
99+
public function activate(CompetencyFramework $competencyFramework): RedirectResponse
100+
{
101+
$this->authorize('activate', $competencyFramework);
102+
103+
$competencyFramework->activate();
104+
105+
return redirect()->route('hr.competency-frameworks.index')
106+
->with('success', 'Competency framework activated.');
107+
}
108+
109+
public function archive(CompetencyFramework $competencyFramework): RedirectResponse
110+
{
111+
$this->authorize('archive', $competencyFramework);
112+
113+
$competencyFramework->archive();
114+
115+
return redirect()->route('hr.competency-frameworks.index')
116+
->with('success', 'Competency framework archived.');
117+
}
118+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace App\Modules\HR\Models;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
7+
8+
class Competency extends Model
9+
{
10+
protected $fillable = [
11+
'competency_framework_id',
12+
'name',
13+
'category',
14+
'description',
15+
'max_level',
16+
];
17+
18+
protected $casts = [
19+
'max_level' => 'integer',
20+
];
21+
22+
protected $attributes = [
23+
'max_level' => 5,
24+
];
25+
26+
public function framework(): BelongsTo
27+
{
28+
return $this->belongsTo(CompetencyFramework::class);
29+
}
30+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
namespace App\Modules\HR\Models;
4+
5+
use App\Modules\Core\Traits\BelongsToTenant;
6+
use Illuminate\Database\Eloquent\Model;
7+
use Illuminate\Database\Eloquent\Relations\HasMany;
8+
use Illuminate\Database\Eloquent\SoftDeletes;
9+
10+
class CompetencyFramework extends Model
11+
{
12+
use BelongsToTenant, SoftDeletes;
13+
14+
protected $fillable = [
15+
'tenant_id',
16+
'name',
17+
'code',
18+
'description',
19+
'status',
20+
'is_default',
21+
'created_by',
22+
];
23+
24+
protected $casts = [
25+
'is_default' => 'boolean',
26+
];
27+
28+
protected $attributes = [
29+
'status' => 'draft',
30+
'is_default' => false,
31+
];
32+
33+
public function competencies(): HasMany
34+
{
35+
return $this->hasMany(Competency::class);
36+
}
37+
38+
public function activate(): void
39+
{
40+
$this->status = 'active';
41+
$this->save();
42+
}
43+
44+
public function archive(): void
45+
{
46+
$this->status = 'archived';
47+
$this->save();
48+
}
49+
50+
public function getIsActiveAttribute(): bool
51+
{
52+
return $this->status === 'active';
53+
}
54+
55+
public function getCompetencyCountAttribute(): int
56+
{
57+
return $this->competencies()->count();
58+
}
59+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
namespace App\Modules\HR\Policies;
4+
5+
use App\Models\User;
6+
use App\Modules\HR\Models\CompetencyFramework;
7+
8+
class CompetencyFrameworkPolicy
9+
{
10+
public function viewAny(User $user): bool
11+
{
12+
return $user->can('hr.view');
13+
}
14+
15+
public function view(User $user, CompetencyFramework $competencyFramework): bool
16+
{
17+
return $user->can('hr.view');
18+
}
19+
20+
public function create(User $user): bool
21+
{
22+
return $user->can('hr.create');
23+
}
24+
25+
public function update(User $user, CompetencyFramework $competencyFramework): bool
26+
{
27+
return $user->can('hr.create');
28+
}
29+
30+
public function activate(User $user, CompetencyFramework $competencyFramework): bool
31+
{
32+
return $user->can('hr.create');
33+
}
34+
35+
public function archive(User $user, CompetencyFramework $competencyFramework): bool
36+
{
37+
return $user->can('hr.create');
38+
}
39+
40+
public function delete(User $user, CompetencyFramework $competencyFramework): bool
41+
{
42+
return $user->can('hr.delete');
43+
}
44+
}

erp/app/Modules/HR/Providers/HRServiceProvider.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@
8282
use App\Modules\HR\Policies\JobOfferPolicy;
8383
use App\Modules\HR\Models\TrainingSession;
8484
use App\Modules\HR\Policies\TrainingSessionPolicy;
85+
use App\Modules\HR\Models\CompetencyFramework;
86+
use App\Modules\HR\Models\Competency;
87+
use App\Modules\HR\Policies\CompetencyFrameworkPolicy;
8588
use Illuminate\Support\Facades\Gate;
8689
use Illuminate\Support\ServiceProvider;
8790

@@ -142,5 +145,7 @@ public function boot(): void
142145
Gate::policy(FlexibleWorkArrangement::class, FlexibleWorkPolicy::class);
143146
Gate::policy(JobOfferLetter::class, JobOfferPolicy::class);
144147
Gate::policy(TrainingSession::class, TrainingSessionPolicy::class);
148+
Gate::policy(CompetencyFramework::class, CompetencyFrameworkPolicy::class);
149+
Gate::policy(Competency::class, CompetencyFrameworkPolicy::class);
145150
}
146151
}

erp/app/Modules/HR/routes/hr.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,3 +294,11 @@
294294
Route::post('training-sessions/{training_session}/cancel', [TrainingSessionController::class, 'cancel'])->name('training-sessions.cancel');
295295
Route::resource('training-sessions', TrainingSessionController::class);
296296
});
297+
298+
// Competency Frameworks
299+
use App\Modules\HR\Http\Controllers\CompetencyFrameworkController;
300+
Route::middleware(['web', 'auth', 'verified'])->prefix('hr')->name('hr.')->group(function () {
301+
Route::post('competency-frameworks/{competency_framework}/activate', [CompetencyFrameworkController::class, 'activate'])->name('competency-frameworks.activate');
302+
Route::post('competency-frameworks/{competency_framework}/archive', [CompetencyFrameworkController::class, 'archive'])->name('competency-frameworks.archive');
303+
Route::resource('competency-frameworks', CompetencyFrameworkController::class);
304+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration
8+
{
9+
public function up(): void
10+
{
11+
Schema::dropIfExists('competency_frameworks');
12+
Schema::create('competency_frameworks', function (Blueprint $table) {
13+
$table->id();
14+
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
15+
$table->string('name');
16+
$table->string('code')->nullable();
17+
$table->text('description')->nullable();
18+
$table->string('status')->default('draft'); // draft/active/archived
19+
$table->boolean('is_default')->default(false);
20+
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
21+
$table->timestamps();
22+
$table->softDeletes();
23+
});
24+
}
25+
26+
public function down(): void
27+
{
28+
Schema::dropIfExists('competency_frameworks');
29+
}
30+
};
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration
8+
{
9+
public function up(): void
10+
{
11+
Schema::dropIfExists('competencies');
12+
Schema::create('competencies', function (Blueprint $table) {
13+
$table->id();
14+
$table->foreignId('competency_framework_id')->constrained()->cascadeOnDelete();
15+
$table->string('name');
16+
$table->string('category')->nullable(); // technical/behavioural/leadership
17+
$table->text('description')->nullable();
18+
$table->integer('max_level')->default(5); // e.g. 1-5 proficiency scale
19+
$table->timestamps();
20+
});
21+
}
22+
23+
public function down(): void
24+
{
25+
Schema::dropIfExists('competencies');
26+
}
27+
};

0 commit comments

Comments
 (0)