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 @@ - + @csrf
@@ -1166,8 +1170,25 @@
+ + + @csrf +
+
+
🚀 Onboarding & Offboarding Module
+
Enable/disable candidate provisioning & 1-click revocation
+
+ +
+ + -
+
@@ -1198,7 +1219,7 @@
-
-
+
+
+ +
-
+
+
+ +