diff --git a/app/Http/Controllers/AdminController.php b/app/Http/Controllers/AdminController.php
index 671acd0..dd5d439 100644
--- a/app/Http/Controllers/AdminController.php
+++ b/app/Http/Controllers/AdminController.php
@@ -156,6 +156,7 @@ public function storeRole(Request $request)
$role = Role::create([
'name' => $request->name,
'description' => $request->description,
+ 'can_manage_onboarding' => $request->boolean('can_manage_onboarding'),
]);
if ($request->has('apps')) {
@@ -182,6 +183,7 @@ public function updateRole(Request $request, $id)
$role->update([
'name' => $request->name,
'description' => $request->description,
+ 'can_manage_onboarding' => $request->boolean('can_manage_onboarding'),
]);
$role->apps()->sync($request->apps ?? []);
@@ -292,6 +294,18 @@ public function updateDefaultRole(Request $request)
return redirect()->route('admin.dashboard')->with('success', 'Default role for new users updated successfully.');
}
+ /**
+ * Toggle the Onboarding & Offboarding module status globally.
+ */
+ public function updateOnboardingToggle(Request $request)
+ {
+ $enabled = $request->boolean('onboarding_enabled') ? '1' : '0';
+ \App\Models\Setting::set('onboarding_enabled', $enabled);
+
+ $msg = $enabled === '1' ? 'Onboarding & Offboarding module enabled.' : 'Onboarding & Offboarding module disabled.';
+ return redirect()->route('admin.dashboard')->with('success', $msg);
+ }
+
/**
* Toggle the admin privilege of a user.
*/
diff --git a/app/Http/Controllers/OnboardingController.php b/app/Http/Controllers/OnboardingController.php
new file mode 100644
index 0000000..e9f9817
--- /dev/null
+++ b/app/Http/Controllers/OnboardingController.php
@@ -0,0 +1,254 @@
+canManageOnboarding()) {
+ return redirect()->route('dashboard')->with('error', 'Access denied. You do not have permission to access Onboarding & Offboarding.');
+ }
+
+ $onboardings = Onboarding::with(['user', 'creator', 'offboarder'])
+ ->orderBy('created_at', 'desc')
+ ->get();
+
+ $activeUsers = User::with(['roles', 'overrides.app'])
+ ->where('id', '!=', Auth::id())
+ ->orderBy('name', 'asc')
+ ->get();
+
+ $apps = App::orderBy('name', 'asc')->get();
+ $roles = Role::where('name', '!=', 'Admin')->orderBy('name', 'asc')->get();
+
+ $stats = [
+ 'preboarding_count' => $onboardings->where('status', 'preboarding')->count(),
+ 'active_interns_count' => $onboardings->where('type', 'intern')->where('status', 'active')->count(),
+ 'offboarded_count' => $onboardings->where('status', 'offboarded')->count(),
+ 'total_managed' => $onboardings->count(),
+ ];
+
+ return view('onboarding.index', compact('onboardings', 'activeUsers', 'apps', 'roles', 'stats'));
+ }
+
+ /**
+ * Store a new onboarding candidate or intern profile.
+ */
+ public function store(Request $request)
+ {
+ $user = Auth::user();
+ if (!$user->canManageOnboarding()) {
+ return redirect()->route('dashboard')->with('error', 'Access denied.');
+ }
+
+ $request->validate([
+ 'name' => 'required|string|max:255',
+ 'email' => 'required|email|max:255',
+ 'type' => 'required|string|in:intern,candidate,employee',
+ 'join_date' => 'nullable|date',
+ 'department' => 'nullable|string|max:255',
+ 'assigned_roles' => 'nullable|array',
+ 'assigned_apps' => 'nullable|array',
+ 'notes' => 'nullable|string',
+ ]);
+
+ $defaultChecklist = [
+ 'document_verified' => false,
+ 'email_created' => false,
+ 'laptop_assigned' => false,
+ 'sso_bundle_granted' => false,
+ 'welcome_briefing' => false,
+ ];
+
+ Onboarding::create([
+ 'name' => $request->name,
+ 'email' => strtolower(trim($request->email)),
+ 'type' => $request->type,
+ 'status' => 'preboarding',
+ 'join_date' => $request->join_date,
+ 'department' => $request->department,
+ 'assigned_roles' => $request->assigned_roles ?? [],
+ 'assigned_apps' => $request->assigned_apps ?? [],
+ 'checklist' => $defaultChecklist,
+ 'notes' => $request->notes,
+ 'created_by' => Auth::id(),
+ ]);
+
+ return redirect()->route('onboarding.index')->with('success', 'Onboarding profile for ' . $request->name . ' created successfully.');
+ }
+
+ /**
+ * Activate Day-1 Candidate / Intern account & provision SSO access.
+ */
+ public function activate($id)
+ {
+ $user = Auth::user();
+ if (!$user->canManageOnboarding()) {
+ return redirect()->route('dashboard')->with('error', 'Access denied.');
+ }
+
+ $onboarding = Onboarding::findOrFail($id);
+
+ // Check if user account already exists by email
+ $existingUser = User::where('email', $onboarding->email)->first();
+
+ if (!$existingUser) {
+ // Create user account with randomized initial password
+ $existingUser = User::create([
+ 'name' => $onboarding->name,
+ 'email' => $onboarding->email,
+ 'password' => Hash::make(Str::random(16)),
+ 'role' => 'user',
+ ]);
+ }
+
+ // Attach assigned roles if any
+ if (!empty($onboarding->assigned_roles)) {
+ $existingUser->roles()->syncWithoutDetaching($onboarding->assigned_roles);
+ } else {
+ // Default role fallback
+ $defaultRoleName = Setting::get('default_role', 'Developer');
+ $defaultRole = Role::firstOrCreate(['name' => $defaultRoleName]);
+ $existingUser->roles()->syncWithoutDetaching([$defaultRole->id]);
+ }
+
+ // Attach pre-approved service overrides
+ if (!empty($onboarding->assigned_apps)) {
+ foreach ($onboarding->assigned_apps as $appId) {
+ UserAppOverride::firstOrCreate([
+ 'user_id' => $existingUser->id,
+ 'app_id' => $appId,
+ 'type' => 'allow',
+ ]);
+ }
+ }
+
+ // Update onboarding record
+ $checklist = $onboarding->checklist ?? [];
+ $checklist['sso_bundle_granted'] = true;
+
+ $onboarding->update([
+ 'user_id' => $existingUser->id,
+ 'status' => 'active',
+ 'checklist' => $checklist,
+ ]);
+
+ return redirect()->route('onboarding.index')->with('success', 'Account activated for ' . $onboarding->name . '. SSO access provisioned!');
+ }
+
+ /**
+ * 1-Click Offboard: Instantly block account, revoke SSO apps, & log offboarding.
+ */
+ public function offboard(Request $request, $id)
+ {
+ $user = Auth::user();
+ if (!$user->canManageOnboarding()) {
+ return redirect()->route('dashboard')->with('error', 'Access denied.');
+ }
+
+ $onboarding = Onboarding::find($id);
+
+ if ($onboarding && $onboarding->user_id) {
+ $targetUser = User::find($onboarding->user_id);
+ } else {
+ // Try to find user directly if $id is user_id
+ $targetUser = User::find($id);
+ if ($targetUser) {
+ $onboarding = Onboarding::firstOrCreate([
+ 'user_id' => $targetUser->id,
+ ], [
+ 'name' => $targetUser->name,
+ 'email' => $targetUser->email,
+ 'type' => 'employee',
+ 'created_by' => Auth::id(),
+ ]);
+ }
+ }
+
+ if (!$targetUser) {
+ return redirect()->route('onboarding.index')->with('error', 'Target user account not found.');
+ }
+
+ // Prevent offboarding self or admins
+ if ($targetUser->id === Auth::id() || $targetUser->isAdmin()) {
+ return redirect()->route('onboarding.index')->with('error', 'Security alert: You cannot offboard administrator accounts.');
+ }
+
+ // 1. Block user account
+ $targetUser->update(['is_blocked' => true]);
+
+ // 2. Clear active overrides / grants
+ $targetUser->overrides()->delete();
+
+ // 3. Update onboarding record
+ $checklist = $onboarding->checklist ?? [];
+ $checklist['exit_revocation_completed'] = true;
+
+ $onboarding->update([
+ 'status' => 'offboarded',
+ 'exit_date' => now(),
+ 'offboarded_by' => Auth::id(),
+ 'offboarded_at' => now(),
+ 'checklist' => $checklist,
+ ]);
+
+ return redirect()->route('onboarding.index')->with('success', '1-Click Offboarding completed for ' . $targetUser->name . '. Account blocked & SSO access revoked!');
+ }
+
+ /**
+ * Update checklist item for an onboarding / offboarding profile.
+ */
+ public function updateChecklist(Request $request, $id)
+ {
+ $user = Auth::user();
+ if (!$user->canManageOnboarding()) {
+ return response()->json(['error' => 'Access denied'], 403);
+ }
+
+ $onboarding = Onboarding::findOrFail($id);
+ $checklist = $onboarding->checklist ?? [];
+
+ $key = $request->input('key');
+ $val = $request->boolean('val');
+
+ if ($key) {
+ $checklist[$key] = $val;
+ $onboarding->update(['checklist' => $checklist]);
+ }
+
+ return response()->json(['success' => true, 'checklist' => $checklist]);
+ }
+
+ /**
+ * Delete an onboarding profile.
+ */
+ public function destroy($id)
+ {
+ $user = Auth::user();
+ if (!$user->canManageOnboarding()) {
+ return redirect()->route('dashboard')->with('error', 'Access denied.');
+ }
+
+ $onboarding = Onboarding::findOrFail($id);
+ $onboarding->delete();
+
+ return redirect()->route('onboarding.index')->with('success', 'Onboarding record removed successfully.');
+ }
+}
diff --git a/app/Models/Onboarding.php b/app/Models/Onboarding.php
new file mode 100644
index 0000000..e932c90
--- /dev/null
+++ b/app/Models/Onboarding.php
@@ -0,0 +1,53 @@
+ 'date',
+ 'exit_date' => 'date',
+ 'offboarded_at' => 'datetime',
+ 'assigned_roles' => 'array',
+ 'assigned_apps' => 'array',
+ 'checklist' => 'array',
+ ];
+
+ public function user()
+ {
+ return $this->belongsTo(User::class, 'user_id');
+ }
+
+ public function creator()
+ {
+ return $this->belongsTo(User::class, 'created_by');
+ }
+
+ public function offboarder()
+ {
+ return $this->belongsTo(User::class, 'offboarded_by');
+ }
+}
diff --git a/app/Models/Role.php b/app/Models/Role.php
index ed66bd4..d98e20d 100644
--- a/app/Models/Role.php
+++ b/app/Models/Role.php
@@ -4,13 +4,21 @@
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
-use Illuminate\Database\Eloquent\Attributes\Fillable;
-#[Fillable(['name', 'description'])]
class Role extends Model
{
use HasFactory;
+ protected $fillable = [
+ 'name',
+ 'description',
+ 'can_manage_onboarding',
+ ];
+
+ protected $casts = [
+ 'can_manage_onboarding' => 'boolean',
+ ];
+
/**
* Get the users assigned to this role.
*/
diff --git a/app/Models/User.php b/app/Models/User.php
index e07425f..136eb41 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -122,6 +122,32 @@ public function authorizedApps()
return App::whereIn('id', $finalAppIds)->orderBy('name', 'asc')->get();
}
+ /**
+ * Check if user is authorized to manage Onboarding & Offboarding.
+ */
+ public function canManageOnboarding(): bool
+ {
+ // 1. Module must be enabled in system settings
+ if (\App\Models\Setting::get('onboarding_enabled', '1') !== '1') {
+ return false;
+ }
+
+ // 2. Admins always have access
+ if ($this->isAdmin()) {
+ return true;
+ }
+
+ // 3. User with 'hr' role or any role having can_manage_onboarding = true
+ if (strtolower($this->role) === 'hr') {
+ return true;
+ }
+
+ return $this->roles()->where(function ($q) {
+ $q->whereRaw('LOWER(name) LIKE ?', ['%hr%'])
+ ->orWhere('can_manage_onboarding', true);
+ })->exists();
+ }
+
/**
* Get the attributes that should be cast.
*
diff --git a/database/migrations/2026_07_20_140115_create_onboardings_table.php b/database/migrations/2026_07_20_140115_create_onboardings_table.php
new file mode 100644
index 0000000..ec8d761
--- /dev/null
+++ b/database/migrations/2026_07_20_140115_create_onboardings_table.php
@@ -0,0 +1,42 @@
+id();
+ $table->foreignId('user_id')->nullable()->constrained('users')->onDelete('set null');
+ $table->string('name');
+ $table->string('email');
+ $table->string('type')->default('intern'); // intern, candidate, employee
+ $table->string('status')->default('preboarding'); // preboarding, active, offboarding, offboarded
+ $table->date('join_date')->nullable();
+ $table->date('exit_date')->nullable();
+ $table->string('department')->nullable();
+ $table->json('assigned_roles')->nullable();
+ $table->json('assigned_apps')->nullable();
+ $table->json('checklist')->nullable();
+ $table->text('notes')->nullable();
+ $table->foreignId('created_by')->nullable()->constrained('users')->onDelete('set null');
+ $table->foreignId('offboarded_by')->nullable()->constrained('users')->onDelete('set null');
+ $table->timestamp('offboarded_at')->nullable();
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('onboardings');
+ }
+};
diff --git a/database/migrations/2026_07_20_141030_add_can_manage_onboarding_to_roles_table.php b/database/migrations/2026_07_20_141030_add_can_manage_onboarding_to_roles_table.php
new file mode 100644
index 0000000..d1fe6c4
--- /dev/null
+++ b/database/migrations/2026_07_20_141030_add_can_manage_onboarding_to_roles_table.php
@@ -0,0 +1,28 @@
+boolean('can_manage_onboarding')->default(false)->after('description');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('roles', function (Blueprint $table) {
+ $table->dropColumn('can_manage_onboarding');
+ });
+ }
+};
diff --git a/public/Screenshot_12.png b/public/Screenshot_12.png
new file mode 100644
index 0000000..b58d7c5
Binary files /dev/null and b/public/Screenshot_12.png differ
diff --git a/public/Screenshot_33.png b/public/Screenshot_33.png
new file mode 100644
index 0000000..1e09f1a
Binary files /dev/null and b/public/Screenshot_33.png differ
diff --git a/resources/views/admin/dashboard.blade.php b/resources/views/admin/dashboard.blade.php
index b645e3a..6488c38 100644
--- a/resources/views/admin/dashboard.blade.php
+++ b/resources/views/admin/dashboard.blade.php
@@ -280,6 +280,7 @@
gap: 30px;
margin-bottom: 50px;
width: 100%;
+ align-items: start;
box-sizing: border-box;
}
@@ -812,6 +813,9 @@
-
+
@@ -1149,7 +1153,7 @@
-
+
+
+
-
+
@@ -1198,7 +1219,7 @@
-
+
+
+
+
-
+
+
+
+
|