Skip to content

Commit 0f5a8e8

Browse files
committed
feat(hr): Phase 121 — Employee Surveys & Pulse Checks
https://claude.ai/code/session_01RdUGwo74JXChRCF88Yu27d
1 parent f26eae4 commit 0f5a8e8

13 files changed

Lines changed: 456 additions & 0 deletions
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
namespace App\Modules\HR\Http\Controllers;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Modules\HR\Models\EmployeeSurvey;
7+
use App\Modules\HR\Models\SurveyResponse;
8+
use Illuminate\Http\RedirectResponse;
9+
use Illuminate\Http\Request;
10+
use Inertia\Inertia;
11+
use Inertia\Response;
12+
13+
class EmployeeSurveyController extends Controller
14+
{
15+
public function index(): Response
16+
{
17+
$this->authorize('viewAny', EmployeeSurvey::class);
18+
$surveys = EmployeeSurvey::where('tenant_id', app('tenant')->id)
19+
->latest()
20+
->paginate(20);
21+
return Inertia::render('HR/Surveys/Index', compact('surveys'));
22+
}
23+
24+
public function store(Request $request): RedirectResponse
25+
{
26+
$this->authorize('create', EmployeeSurvey::class);
27+
$validated = $request->validate([
28+
'title' => 'required|string|max:255',
29+
'description' => 'nullable|string',
30+
'start_date' => 'nullable|date',
31+
'end_date' => 'nullable|date|after_or_equal:start_date',
32+
'is_anonymous' => 'nullable|boolean',
33+
]);
34+
$validated['tenant_id'] = app('tenant')->id;
35+
$validated['created_by'] = auth()->id();
36+
EmployeeSurvey::create($validated);
37+
return back()->with('success', 'Survey created.');
38+
}
39+
40+
public function show(EmployeeSurvey $survey): Response
41+
{
42+
$this->authorize('view', $survey);
43+
return Inertia::render('HR/Surveys/Show', compact('survey'));
44+
}
45+
46+
public function publish(EmployeeSurvey $survey): RedirectResponse
47+
{
48+
$this->authorize('update', $survey);
49+
$survey->publish();
50+
return back()->with('success', 'Survey published.');
51+
}
52+
53+
public function close(EmployeeSurvey $survey): RedirectResponse
54+
{
55+
$this->authorize('update', $survey);
56+
$survey->close();
57+
return back()->with('success', 'Survey closed.');
58+
}
59+
60+
public function respond(Request $request, EmployeeSurvey $survey): RedirectResponse
61+
{
62+
$validated = $request->validate([
63+
'answers' => 'required|array',
64+
'employee_id' => 'nullable|exists:employees,id',
65+
]);
66+
SurveyResponse::create([
67+
'tenant_id' => app('tenant')->id,
68+
'employee_survey_id' => $survey->id,
69+
'employee_id' => $validated['employee_id'] ?? null,
70+
'answers' => $validated['answers'],
71+
'submitted_at' => now(),
72+
]);
73+
return back()->with('success', 'Response submitted.');
74+
}
75+
76+
public function destroy(EmployeeSurvey $survey): RedirectResponse
77+
{
78+
$this->authorize('delete', $survey);
79+
$survey->delete();
80+
return back()->with('success', 'Survey deleted.');
81+
}
82+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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 EmployeeSurvey extends Model
11+
{
12+
use BelongsToTenant, SoftDeletes;
13+
14+
protected $attributes = [
15+
'status' => 'draft',
16+
'is_anonymous' => false,
17+
];
18+
19+
protected $fillable = [
20+
'tenant_id', 'title', 'description', 'status',
21+
'start_date', 'end_date', 'is_anonymous', 'created_by',
22+
];
23+
24+
protected $casts = [
25+
'start_date' => 'date',
26+
'end_date' => 'date',
27+
'is_anonymous' => 'boolean',
28+
];
29+
30+
public function publish(): void
31+
{
32+
$this->update(['status' => 'published']);
33+
}
34+
35+
public function close(): void
36+
{
37+
$this->update(['status' => 'closed']);
38+
}
39+
40+
public function getIsActiveAttribute(): bool
41+
{
42+
return $this->status === 'published'
43+
&& ($this->end_date === null || $this->end_date->gte(now()->startOfDay()));
44+
}
45+
46+
public function getResponseCountAttribute(): int
47+
{
48+
return $this->responses()->count();
49+
}
50+
51+
public function questions(): HasMany
52+
{
53+
return $this->hasMany(SurveyQuestion::class, 'employee_survey_id');
54+
}
55+
56+
public function responses(): HasMany
57+
{
58+
return $this->hasMany(SurveyResponse::class, 'employee_survey_id');
59+
}
60+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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\BelongsTo;
8+
9+
class SurveyQuestion extends Model
10+
{
11+
use BelongsToTenant;
12+
13+
protected $fillable = [
14+
'tenant_id', 'employee_survey_id', 'question_text',
15+
'question_type', 'options', 'sort_order', 'is_required',
16+
];
17+
18+
protected $casts = [
19+
'options' => 'array',
20+
'is_required' => 'boolean',
21+
];
22+
23+
public function survey(): BelongsTo
24+
{
25+
return $this->belongsTo(EmployeeSurvey::class, 'employee_survey_id');
26+
}
27+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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\BelongsTo;
8+
9+
class SurveyResponse extends Model
10+
{
11+
use BelongsToTenant;
12+
13+
protected $fillable = [
14+
'tenant_id', 'employee_survey_id', 'employee_id', 'answers', 'submitted_at',
15+
];
16+
17+
protected $casts = [
18+
'answers' => 'array',
19+
'submitted_at' => 'datetime',
20+
];
21+
22+
public function survey(): BelongsTo
23+
{
24+
return $this->belongsTo(EmployeeSurvey::class, 'employee_survey_id');
25+
}
26+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace App\Modules\HR\Policies;
4+
5+
use App\Models\User;
6+
use App\Modules\HR\Models\EmployeeSurvey;
7+
8+
class EmployeeSurveyPolicy
9+
{
10+
public function viewAny(User $user): bool
11+
{
12+
return $user->hasPermissionTo('hr.view');
13+
}
14+
15+
public function view(User $user, EmployeeSurvey $survey): bool
16+
{
17+
return $user->hasPermissionTo('hr.view');
18+
}
19+
20+
public function create(User $user): bool
21+
{
22+
return $user->hasPermissionTo('hr.create');
23+
}
24+
25+
public function update(User $user, EmployeeSurvey $survey): bool
26+
{
27+
return $user->hasPermissionTo('hr.create');
28+
}
29+
30+
public function delete(User $user, EmployeeSurvey $survey): bool
31+
{
32+
return $user->hasPermissionTo('hr.delete');
33+
}
34+
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@
7474
use App\Modules\HR\Policies\OvertimeRequestPolicy;
7575
use App\Modules\HR\Models\SalaryGrade;
7676
use App\Modules\HR\Policies\SalaryGradePolicy;
77+
use App\Modules\HR\Models\EmployeeSurvey;
78+
use App\Modules\HR\Policies\EmployeeSurveyPolicy;
7779
use Illuminate\Support\Facades\Gate;
7880
use Illuminate\Support\ServiceProvider;
7981

@@ -130,5 +132,6 @@ public function boot(): void
130132
Gate::policy(EmployeePositionChange::class, PositionChangePolicy::class);
131133
Gate::policy(SalaryGrade::class, SalaryGradePolicy::class);
132134
Gate::policy(OvertimeRequest::class, OvertimeRequestPolicy::class);
135+
Gate::policy(EmployeeSurvey::class, EmployeeSurveyPolicy::class);
133136
}
134137
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,3 +258,12 @@
258258
Route::post('overtime-requests/{overtimeRequest}/cancel', [OvertimeRequestController::class, 'cancel'])->name('overtime-requests.cancel');
259259
Route::resource('overtime-requests', OvertimeRequestController::class)->only(['index', 'store', 'show', 'destroy']);
260260
});
261+
262+
// Employee Surveys
263+
use App\Modules\HR\Http\Controllers\EmployeeSurveyController;
264+
Route::middleware(['web', 'auth', 'verified'])->prefix('hr')->name('hr.')->group(function () {
265+
Route::post('surveys/{survey}/publish', [EmployeeSurveyController::class, 'publish'])->name('surveys.publish');
266+
Route::post('surveys/{survey}/close', [EmployeeSurveyController::class, 'close'])->name('surveys.close');
267+
Route::post('surveys/{survey}/respond', [EmployeeSurveyController::class, 'respond'])->name('surveys.respond');
268+
Route::resource('surveys', EmployeeSurveyController::class)->only(['index', 'store', 'show', 'destroy']);
269+
});
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::create('employee_surveys', function (Blueprint $table) {
12+
$table->id();
13+
$table->unsignedBigInteger('tenant_id');
14+
$table->string('title');
15+
$table->text('description')->nullable();
16+
$table->string('status')->default('draft'); // draft/published/closed
17+
$table->date('start_date')->nullable();
18+
$table->date('end_date')->nullable();
19+
$table->boolean('is_anonymous')->default(false);
20+
$table->unsignedBigInteger('created_by')->nullable();
21+
$table->timestamps();
22+
$table->softDeletes();
23+
});
24+
}
25+
26+
public function down(): void
27+
{
28+
Schema::dropIfExists('employee_surveys');
29+
}
30+
};
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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::create('survey_questions', function (Blueprint $table) {
12+
$table->id();
13+
$table->unsignedBigInteger('tenant_id');
14+
$table->unsignedBigInteger('employee_survey_id');
15+
$table->string('question_text');
16+
$table->string('question_type')->default('text'); // text/rating/yes_no/multiple_choice
17+
$table->json('options')->nullable(); // for multiple_choice
18+
$table->integer('sort_order')->default(0);
19+
$table->boolean('is_required')->default(true);
20+
$table->timestamps();
21+
});
22+
}
23+
24+
public function down(): void
25+
{
26+
Schema::dropIfExists('survey_questions');
27+
}
28+
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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::create('survey_responses', function (Blueprint $table) {
12+
$table->id();
13+
$table->unsignedBigInteger('tenant_id');
14+
$table->unsignedBigInteger('employee_survey_id');
15+
$table->unsignedBigInteger('employee_id')->nullable(); // null if anonymous
16+
$table->json('answers'); // [{question_id: X, answer: "..."}]
17+
$table->timestamp('submitted_at')->nullable();
18+
$table->timestamps();
19+
});
20+
}
21+
22+
public function down(): void
23+
{
24+
Schema::dropIfExists('survey_responses');
25+
}
26+
};

0 commit comments

Comments
 (0)