filled('search')) { $search = $request->search; $query->where(function ($q) use ($search) { $q->where('name', 'like', "%{$search}%") ->orWhere('email', 'like', "%{$search}%"); }); } if ($request->filled('type') && $request->type !== 'all') { $query->ofType($request->type); } if ($request->filled('status') && $request->status !== 'all') { $query->where('status', $request->status); } $users = $query->latest()->paginate(15)->withQueryString(); return Inertia::render('UserManagement::Users/Index', [ 'users' => $users, 'filters' => $request->only(['search', 'type', 'status']), ]); } public function create(): Response { $isPlatformAdmin = is_null(auth()->user()->contractor_id); return Inertia::render('UserManagement::Users/Create', [ 'contractors' => $isPlatformAdmin ? Contractor::where('status', 'active') ->orderBy('company_name') ->get(['id', 'company_name', 'type']) : [], ]); } public function store(Request $request): RedirectResponse { $isPlatformAdmin = is_null(auth()->user()->contractor_id); $allowedRoles = $isPlatformAdmin ? ['admin', 'contractor', 'employee', 'customer'] : ['contractor', 'employee', 'customer']; $validated = $request->validate([ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], 'user_type' => ['required', Rule::in($allowedRoles)], 'status' => ['required', Rule::in(['active', 'inactive', 'suspended'])], 'invite_method' => ['required', Rule::in(['direct', 'email'])], 'auto_password' => ['nullable', 'boolean'], 'password' => ['nullable', 'string', 'min:8', 'confirmed', 'required_if:auto_password,false'], // Platform admin only: pick the contractor for this user 'contractor_id' => $isPlatformAdmin ? ['nullable', 'exists:contractors,id'] : ['prohibited'], // Employee fields 'department' => ['nullable', 'string', 'max:255'], 'position' => ['nullable', 'string', 'max:255'], 'employee_code' => ['nullable', 'string', 'max:255', 'unique:employee_profiles'], 'hire_date' => ['nullable', 'date'], 'phone' => ['nullable', 'string', 'max:50'], 'address' => ['nullable', 'string'], // Customer fields 'company_name' => ['nullable', 'string', 'max:255'], 'contact_person' => ['nullable', 'string', 'max:255'], 'tin_number' => ['nullable', 'string', 'max:255'], 'spatie_role' => ['nullable', 'string', Rule::in(['Super Admin', 'Owner', 'Contractor', 'Project Manager', 'Designer', 'Site Technical', 'Construction Supervisor'])], ]); // Resolve contractor_id: contractor admins always use their own; // platform admins use the one selected in the form (null = platform user) $contractorId = $isPlatformAdmin ? ($validated['contractor_id'] ?? null) : auth()->user()->contractor_id; // Resolve temporary password $plainPassword = $request->boolean('auto_password') ? Str::random(10) : $validated['password']; $user = User::create([ 'name' => $validated['name'], 'email' => $validated['email'], 'password' => Hash::make($plainPassword), 'user_type' => $validated['user_type'], 'status' => $validated['status'], 'contractor_id' => $contractorId, 'must_change_password' => true, ]); // Determine Spatie role to assign $roleToAssign = $validated['spatie_role'] ?? $validated['user_type']; if ($roleToAssign === 'admin') { $roleToAssign = 'Super Admin'; } // Explicitly assign the spatie role matching the user type or selected role $user->assignRole($roleToAssign); // Create profile $this->createProfile($user, $validated); // Deliver credentials if ($validated['invite_method'] === 'email') { $this->deliverByEmail($user, $plainPassword); } $flashMessage = $validated['invite_method'] === 'direct' ? "User created. Temporary password: {$plainPassword}" : "User invited. Credentials sent to {$user->email}."; return redirect()->route('users.index')->with('success', $flashMessage); } public function show(User $user): Response { $this->authorizeScopedUser($user); $user->load(['roles', 'permissions', 'employeeProfile', 'customerProfile']); return Inertia::render('UserManagement::Users/Show', [ 'user' => $user, ]); } public function edit(User $user): Response { $this->authorizeScopedUser($user); $user->load(['employeeProfile', 'customerProfile', 'contractor']); $isPlatformAdmin = is_null(auth()->user()->contractor_id); return Inertia::render('UserManagement::Users/Edit', [ 'user' => $user, 'contractors' => $isPlatformAdmin ? Contractor::where('status', 'active') ->orderBy('company_name') ->get(['id', 'company_name', 'type']) : [], 'isPlatformAdmin' => $isPlatformAdmin, ]); } public function update(Request $request, User $user): RedirectResponse { $this->authorizeScopedUser($user); $isPlatformAdmin = is_null(auth()->user()->contractor_id); $allowedRoles = $isPlatformAdmin ? ['admin', 'contractor', 'employee', 'customer'] : ['contractor', 'employee', 'customer']; $validated = $request->validate([ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'string', 'email', 'max:255', Rule::unique('users')->ignore($user->id)], 'password' => ['nullable', 'string', 'min:8', 'confirmed'], 'user_type' => ['required', Rule::in($allowedRoles)], 'status' => ['required', Rule::in(['active', 'inactive', 'suspended'])], 'department' => ['nullable', 'string', 'max:255'], 'position' => ['nullable', 'string', 'max:255'], 'employee_code' => ['nullable', 'string', 'max:255', Rule::unique('employee_profiles')->ignore($user->employeeProfile?->id)], 'hire_date' => ['nullable', 'date'], 'phone' => ['nullable', 'string', 'max:50'], 'address' => ['nullable', 'string'], 'company_name' => ['nullable', 'string', 'max:255'], 'contact_person'=> ['nullable', 'string', 'max:255'], 'tin_number' => ['nullable', 'string', 'max:255'], 'spatie_role' => ['nullable', 'string', Rule::in(['Super Admin', 'Owner', 'Contractor', 'Project Manager', 'Designer', 'Site Technical', 'Construction Supervisor'])], ]); $userData = [ 'name' => $validated['name'], 'email' => $validated['email'], 'user_type' => $validated['user_type'], 'status' => $validated['status'], ]; if (! empty($validated['password'])) { $userData['password'] = Hash::make($validated['password']); } $user->update($userData); $roleToAssign = $validated['spatie_role'] ?? $validated['user_type']; if ($roleToAssign === 'admin') { $roleToAssign = 'Super Admin'; } $user->syncRoles([$roleToAssign]); // Update profile if (in_array($validated['user_type'], ['admin', 'employee', 'contractor'])) { $profile = $user->employeeProfile()->updateOrCreate( ['user_id' => $user->id], [ 'department' => $validated['department'] ?? null, 'position' => $validated['position'] ?? null, 'employee_code' => $validated['employee_code'] ?? null, 'hire_date' => $validated['hire_date'] ?? null, 'phone' => $validated['phone'] ?? null, 'address' => $validated['address'] ?? null, ] ); $user->update(['userable_type' => EmployeeProfile::class, 'userable_id' => $profile->id]); } elseif ($validated['user_type'] === 'customer') { $profile = $user->customerProfile()->updateOrCreate( ['user_id' => $user->id], [ 'company_name' => $validated['company_name'] ?? null, 'contact_person'=> $validated['contact_person'] ?? null, 'tin_number' => $validated['tin_number'] ?? null, 'phone' => $validated['phone'] ?? null, 'address' => $validated['address'] ?? null, ] ); $user->update(['userable_type' => CustomerProfile::class, 'userable_id' => $profile->id]); } return redirect()->route('users.index')->with('success', 'User updated successfully.'); } public function destroy(User $user): RedirectResponse { $this->authorizeScopedUser($user); if ($user->id === auth()->id()) { return back()->with('error', 'You cannot delete your own account.'); } $user->delete(); return redirect()->route('users.index')->with('success', 'User deleted successfully.'); } public function toggleStatus(User $user): RedirectResponse { $this->authorizeScopedUser($user); if ($user->id === auth()->id()) { return back()->with('error', 'You cannot change your own status.'); } $newStatus = $user->status === 'active' ? 'suspended' : 'active'; $user->update(['status' => $newStatus]); return back()->with('success', "User status changed to {$newStatus}."); } public function linkContractor(Request $request, User $user): RedirectResponse { // Only platform admins may link/unlink contractors abort_unless(is_null(auth()->user()->contractor_id), 403, 'Only platform admins can link contractors.'); $this->authorizeScopedUser($user); $validated = $request->validate([ 'contractor_id' => ['nullable', 'exists:contractors,id'], ]); $user->update(['contractor_id' => $validated['contractor_id'] ?? null]); $message = $validated['contractor_id'] ? 'Contractor linked to user successfully.' : 'Contractor link removed from user.'; return back()->with('success', $message); } // ─── Bulk Import ────────────────────────────────────────────────────────── public function bulkImportForm(): Response { $isPlatformAdmin = is_null(auth()->user()->contractor_id); return Inertia::render('UserManagement::Users/BulkImport', [ 'contractors' => $isPlatformAdmin ? Contractor::where('status', 'active') ->orderBy('company_name') ->get(['id', 'company_name', 'type']) : [], ]); } public function bulkImport(Request $request): Response|RedirectResponse { $isPlatformAdmin = is_null(auth()->user()->contractor_id); $request->validate([ 'file' => ['required', 'file', 'mimes:csv,txt', 'max:2048'], 'invite_method' => ['required', Rule::in(['direct', 'email'])], 'contractor_id' => $isPlatformAdmin ? ['nullable', 'exists:contractors,id'] : ['prohibited'], ]); $contractorId = $isPlatformAdmin ? ($request->contractor_id ?? null) : auth()->user()->contractor_id; $file = $request->file('file'); $handle = fopen($file->getRealPath(), 'r'); $headers = fgetcsv($handle); // skip header row if (! $headers) { return back()->with('error', 'CSV file is empty or unreadable.'); } $created = []; $errors = []; $rowNum = 1; while (($row = fgetcsv($handle)) !== false) { $rowNum++; if (count($row) < 2) { $errors[] = "Row {$rowNum}: Not enough columns."; continue; } [$name, $email, $role, $department, $position] = array_pad($row, 5, null); $name = trim($name); $email = trim($email); $role = trim($role ?: 'employee'); $department= trim($department ?? ''); $position = trim($position ?? ''); if (! filter_var($email, FILTER_VALIDATE_EMAIL)) { $errors[] = "Row {$rowNum}: Invalid email '{$email}'."; continue; } if (User::where('email', $email)->exists()) { $errors[] = "Row {$rowNum}: Email '{$email}' already exists."; continue; } $allowedRoles = ['Super Admin', 'Owner', 'Contractor', 'Project Manager', 'Designer', 'Site Technical', 'Construction Supervisor']; $resolvedRole = in_array($role, $allowedRoles) ? $role : 'Designer'; // Map the Spatie role to the base user_type $userType = $resolvedRole; if (in_array($resolvedRole, ['Contractor', 'Project Manager', 'Designer', 'Site Technical', 'Construction Supervisor'])) { $userType = 'contractor'; } $plainPassword = Str::random(10); $user = User::create([ 'name' => $name, 'email' => $email, 'password' => Hash::make($plainPassword), 'user_type' => $userType, 'status' => 'active', 'contractor_id' => $contractorId, 'must_change_password' => true, ]); $user->assignRole($resolvedRole); if (in_array($userType, ['admin', 'employee', 'contractor'])) { $profile = EmployeeProfile::create([ 'user_id' => $user->id, 'department' => $department ?: null, 'position' => $position ?: null, ]); $user->update(['userable_type' => EmployeeProfile::class, 'userable_id' => $profile->id]); } if ($request->invite_method === 'email') { $this->deliverByEmail($user, $plainPassword); $created[] = ['name' => $name, 'email' => $email, 'role' => $resolvedRole, 'password' => '(sent via email)']; } else { $created[] = ['name' => $name, 'email' => $email, 'role' => $resolvedRole, 'password' => $plainPassword]; } } fclose($handle); return Inertia::render('UserManagement::Users/BulkImport', [ 'results' => [ 'created' => $created, 'errors' => $errors, ], ]); } // ─── Forced Password Change ──────────────────────────────────────────────── public function changePasswordForm(): Response { return Inertia::render('UserManagement::Users/ChangePassword'); } public function changePassword(Request $request): RedirectResponse { $request->validate([ 'password' => ['required', 'string', 'min:8', 'confirmed'], ]); auth()->user()->update([ 'password' => Hash::make($request->password), 'must_change_password' => false, ]); return redirect()->route('dashboard')->with('success', 'Password updated successfully. Welcome aboard!'); } // ─── Helpers ────────────────────────────────────────────────────────────── /** * Redirect contractor admins away from users outside their scope (friendly 404 alternative). */ private function authorizeScopedUser(User $user): void { $authUser = auth()->user(); if (! is_null($authUser->contractor_id) && $user->contractor_id !== $authUser->contractor_id) { abort(redirect()->route('users.index')->with('error', 'You do not have permission to access this user.')); } } private function createProfile(User $user, array $validated): void { if (in_array($validated['user_type'], ['admin', 'employee', 'contractor'])) { $profile = EmployeeProfile::create([ 'user_id' => $user->id, 'department' => $validated['department'] ?? null, 'position' => $validated['position'] ?? null, 'employee_code' => $validated['employee_code'] ?? null, 'hire_date' => $validated['hire_date'] ?? null, 'phone' => $validated['phone'] ?? null, 'address' => $validated['address'] ?? null, ]); $user->update(['userable_type' => EmployeeProfile::class, 'userable_id' => $profile->id]); } elseif ($validated['user_type'] === 'customer') { $profile = CustomerProfile::create([ 'user_id' => $user->id, 'company_name' => $validated['company_name'] ?? null, 'contact_person'=> $validated['contact_person'] ?? null, 'tin_number' => $validated['tin_number'] ?? null, 'phone' => $validated['phone'] ?? null, 'address' => $validated['address'] ?? null, ]); $user->update(['userable_type' => CustomerProfile::class, 'userable_id' => $profile->id]); } } private function deliverByEmail(User $user, string $plainPassword): void { try { Mail::raw( "Hello {$user->name},\n\nYou have been added to the system.\n\nEmail: {$user->email}\nTemporary Password: {$plainPassword}\n\nPlease log in and change your password immediately.", fn ($m) => $m->to($user->email)->subject('Your Account Credentials') ); } catch (\Exception $e) { // Fallback: log credentials if mail is not configured Log::info("User invite credentials (mail not configured) — Email: {$user->email} | Temp Password: {$plainPassword}"); } } }