roleModel = new RoleModel(); $this->userModel = new UserModel(); $this->userRoleModel = new UserRoleModel(); $this->staffModel = new StaffModel(); $this->configModel = new ConfigurationModel(); $this->permissionModel = new PermissionModel(); $this->rolePermissionModel = new RolePermissionModel(); $this->request = \Config\Services::request(); $this->db = \Config\Database::connect(); $this->schoolYear = $this->configModel->getConfig('school_year'); $this->semester = $this->configModel->getConfig('semester'); } public function index() { helper('url'); return view('rolepermission/list_roles', [ 'rolesEndpoint' => site_url('api/rolepermission/roles'), ]); } public function assignRole() { helper('url'); return view('rolepermission/assign_role', [ 'assignRoleEndpoint' => site_url('api/rolepermission/users'), 'rolesEndpoint' => site_url('api/rolepermission/roles'), ]); } public function saveRole() { if ($this->request->getMethod(true) === 'POST') { $userId = (int) $this->request->getPost('user_id'); $roleIdsRaw = $this->request->getPost('role_ids'); $roleIdsArray = is_array($roleIdsRaw) ? $roleIdsRaw : ($roleIdsRaw !== null ? [$roleIdsRaw] : []); $roleIds = array_values(array_unique(array_filter(array_map('intval', $roleIdsArray), fn($id) => $id > 0))); $adminId = session()->get('user_id'); if ($userId <= 0) { return redirect()->to('rolepermission/assign_role')->with('error', 'Invalid user selection.'); } try { $this->db->transStart(); // Remove all existing roles for the user $this->userRoleModel->where('user_id', $userId)->delete(); // Assign new roles if (!empty($roleIds)) { foreach ($roleIds as $roleId) { $this->userRoleModel->insert([ 'user_id' => $userId, 'role_id' => $roleId, 'created_at' => utc_now(), 'updated_by' => $adminId ]); } } $this->db->transComplete(); if ($this->db->transStatus() === false) { throw new \RuntimeException('Transaction failed.'); } // ✅ Load role names after saving $assignedRoles = !empty($roleIds) ? $this->roleModel->whereIn('id', $roleIds)->findAll() : []; $roleNames = array_map(fn($r) => strtolower($r['name']), $assignedRoles ?? []); // ✅ Update staff record if needed $this->updateStaffRecord($userId, $roleNames); return redirect()->to('rolepermission/assign_role')->with('success', 'Roles updated successfully.'); } catch (\Exception $e) { $this->db->transRollback(); log_message('error', 'Error updating roles: ' . $e->getMessage()); return redirect()->to('rolepermission/assign_role')->with('error', 'Failed to update roles.'); } } return redirect()->to('rolepermission/assign_role')->with('error', 'Invalid request.'); } public function usersData() { return $this->response->setJSON($this->buildUsersWithRolesPayload()); } public function rolesData() { $rows = $this->roleModel ->select('roles.id, roles.name, roles.description, COUNT(DISTINCT user_roles.user_id) AS user_count') ->join('user_roles', 'user_roles.role_id = roles.id', 'left') ->groupBy('roles.id, roles.name, roles.description') ->orderBy('roles.name', 'ASC') ->findAll(); $normalized = array_map(static function ($row) { if (!is_array($row)) { return []; } return [ 'id' => (int) ($row['id'] ?? 0), 'name' => (string) ($row['name'] ?? ''), 'description' => $row['description'] ?? null, 'user_count' => (int) ($row['user_count'] ?? 0), ]; }, $rows ?? []); return $this->response->setJSON(['roles' => $normalized]); } private function buildUsersWithRolesPayload(): array { $rows = $this->userModel ->select('users.id, users.firstname, users.lastname, users.email, users.account_id, roles.name AS role_name') ->join('user_roles', 'users.id = user_roles.user_id', 'left') ->join('roles', 'user_roles.role_id = roles.id', 'left') ->orderBy('users.id', 'ASC') ->findAll(); $users = []; foreach ($rows as $row) { if (!is_array($row)) { continue; } $id = (int) ($row['id'] ?? 0); if ($id <= 0) { continue; } if (!isset($users[$id])) { $users[$id] = [ 'id' => $id, 'firstname' => $row['firstname'] ?? '', 'lastname' => $row['lastname'] ?? '', 'email' => $row['email'] ?? '', 'account_id' => $row['account_id'] ?? '', 'roles' => [], ]; } $roleName = $row['role_name'] ?? null; if ($roleName) { $users[$id]['roles'][] = $roleName; } } $normalized = array_map(static function ($user) { $roles = array_values(array_unique(array_filter($user['roles'] ?? [], 'strlen'))); return [ 'id' => $user['id'], 'firstname' => $user['firstname'] ?? '', 'lastname' => $user['lastname'] ?? '', 'email' => $user['email'] ?? '', 'account_id' => $user['account_id'] ?? '', 'roles' => $roles, ]; }, array_values($users)); return ['users' => $normalized]; } private function updateStaffRecord(int $userId, array $newRoleNames): void { $excludedRoles = ['parent', 'student', 'guest']; $loweredNewRoles = array_map('strtolower', $newRoleNames); $user = $this->userModel->find($userId); if (!$user) { log_message('error', "updateStaffRecord: user_id {$userId} not found"); return; } // Fetch existing staff record if it exists $existingStaff = $this->staffModel->where('user_id', $userId)->first(); // Extract existing roles from DB if any $existingRoles = []; if (!empty($existingStaff['role_name'])) { $existingRoles = array_map('strtolower', array_map('trim', explode(',', $existingStaff['role_name']))); } // Merge and deduplicate roles $allRoles = array_unique(array_merge($existingRoles, $loweredNewRoles)); // Determine staff roles (excluding parent/student/guest) $staffRoles = array_filter($newRoleNames, fn($role) => !in_array($role, $excludedRoles)); // Determine active role $activeRole = !empty($staffRoles) ? $staffRoles[0] : 'inactive'; $email = $this->generateUniqueStaffEmail($user['firstname'], $user['lastname'], $userId); $now = utc_now(); $row = [ 'user_id' => $userId, 'firstname' => $user['firstname'], 'lastname' => $user['lastname'], 'email' => $email, 'phone' => $user['cellphone'], 'role_name' => implode(', ', $allRoles), // historical roles 'active_role' => $activeRole, 'school_year' => $this->schoolYear, 'updated_at' => $now, ]; $this->staffModel->upsert($row); } private function generateUniqueStaffEmail(string $firstname, string $lastname, int $userId): string { $domain = '@alrahmaisgl.org'; // Sanitize names: lowercase, letters only $firstname = strtolower(preg_replace('/[^a-z]/i', '', $firstname)); $lastname = strtolower(preg_replace('/[^a-z]/i', '', $lastname)); // Use at least 1 letter from firstname $min = 1; $max = strlen($firstname); $base = ''; $email = ''; for ($i = $min; $i <= $max; $i++) { $base = substr($firstname, 0, $i) . $lastname; $email = $base . $domain; // If email not taken or belongs to same user, use it $exists = $this->staffModel->where('email', $email) ->where('user_id !=', $userId) ->first(); if (!$exists) { return $email; } } // If all combinations used, fallback to adding user ID return $base . $userId . $domain; } public function listRoles() { helper('url'); return view('rolepermission/list_roles', [ 'rolesEndpoint' => site_url('api/rolepermission/roles'), ]); } public function editRolePermissions($roleId) { // Fetch roles, permissions, and role permissions data $roles = $this->roleModel->findAll(); $permissions = $this->permissionModel->findAll(); $rolePermissions = $this->rolePermissionModel->where('role_id', $roleId)->findAll(); // Convert existing role permissions to a simple array for comparison $existingPermissions = []; foreach ($rolePermissions as $rolePermission) { $existingPermissions[$rolePermission['permission_id']] = [ 'can_create' => $rolePermission['can_create'], 'can_read' => $rolePermission['can_read'], 'can_update' => $rolePermission['can_update'], 'can_delete' => $rolePermission['can_delete'], ]; } // Handle POST request if (strtolower($this->request->getMethod()) === 'post') { // Get the posted permissions data $postPermissions = $this->request->getPost('permissions'); // Call saveRolePermissions with the correct data return $this->saveRolePermissions($roleId, $postPermissions); } return view('rolepermission/edit_role_permissions', [ 'roles' => $roles, 'roleId' => $roleId, 'permissions' => $permissions, 'rolePermissions' => $rolePermissions, 'existingPermissions' => $existingPermissions ]); } public function saveRolePermissions($roleId = null, $permissions = null) { // If $roleId and $permissions are not passed, retrieve them from POST if (!$roleId) { $roleId = $this->request->getPost('role_id'); } if (!$permissions) { $permissions = $this->request->getPost('permissions'); } if (!$roleId) { return redirect()->back()->with('error', 'Role ID is missing.'); } // Fetch existing permissions for the role $rolePermissions = $this->rolePermissionModel->where('role_id', $roleId)->findAll(); $existingPermissions = []; foreach ($rolePermissions as $rolePermission) { $existingPermissions[$rolePermission['permission_id']] = $rolePermission; } try { $this->rolePermissionModel->transStart(); // Process the new permissions if provided if ($permissions && is_array($permissions)) { foreach ($permissions as $permissionId => $crudValues) { // Ensure CRUD values are correctly set, defaulting to 0 if not provided $permissionData = [ 'can_create' => isset($crudValues['create']) ? 1 : 0, 'can_read' => isset($crudValues['read']) ? 1 : 0, 'can_update' => isset($crudValues['update']) ? 1 : 0, 'can_delete' => isset($crudValues['delete']) ? 1 : 0, 'updated_at' => utc_now(), ]; if (isset($existingPermissions[$permissionId])) { // Update existing permission $this->rolePermissionModel->where('role_id', $roleId) ->where('permission_id', $permissionId) ->set($permissionData) ->update(); } else { // Insert new permission $permissionData['role_id'] = $roleId; $permissionData['permission_id'] = $permissionId; $permissionData['created_at'] = utc_now(); $this->rolePermissionModel->insert($permissionData); } } } // Remove permissions that are no longer present foreach ($existingPermissions as $permissionId => $permissionData) { if (!isset($permissions[$permissionId])) { $this->rolePermissionModel->where('role_id', $roleId) ->where('permission_id', $permissionId) ->delete(); } } $this->rolePermissionModel->transComplete(); if ($this->rolePermissionModel->transStatus() === false) { throw new \RuntimeException('Transaction failed.'); } return redirect()->to('/rolepermission/edit_role_permissions/' . $roleId) ->with('status', 'Permissions saved successfully.'); } catch (\Exception $e) { $this->rolePermissionModel->transRollback(); log_message('error', 'Failed to save permissions: ' . $e->getMessage()); return redirect()->back()->with('error', 'Failed to save permissions: ' . $e->getMessage()); } } // Role management functions public function rolesList() { return $this->listRoles(); } public function addRole() { return view('rolepermission/add_role'); } public function storeRole() { $validation = \Config\Services::validation(); // Validation rules $validation->setRules([ 'name' => 'required|min_length[3]|max_length[255]|is_unique[roles.name]', 'slug' => 'permit_empty|max_length[64]|is_unique[roles.slug]', 'description' => 'permit_empty|max_length[500]', 'dashboard_route' => [ 'rules' => 'required|min_length[1]|max_length[255]|regex_match[/^[a-z0-9_\\/\\-]+$/]', 'errors' => [ 'regex_match' => 'Route may contain lowercase letters, numbers, /, -, and _.' ] ], 'priority' => 'required|integer|greater_than_equal_to[0]|less_than_equal_to[100000]', 'is_active' => 'required|in_list[0,1]', ]); if (!$validation->withRequest($this->request)->run()) { return redirect()->back()->withInput()->with('errors', $validation->getErrors()); } // Normalize + sanitize inputs $rawName = trim((string) $this->request->getPost('name')); $rawSlug = trim((string) $this->request->getPost('slug')); $route = trim((string) $this->request->getPost('dashboard_route')); $desc = trim((string) $this->request->getPost('description')); $priority = (int) $this->request->getPost('priority'); $isActive = (string) $this->request->getPost('is_active') === '1' ? 1 : 0; // Force lowercase for consistency $name = strtolower($rawName); $dashboard_route = strtolower($route); $description = strtolower($desc); // Build slug: from input if provided, else from name $slug = $rawSlug !== '' ? $rawSlug : $name; // convert spaces / invalid chars to underscores and lowercase $slug = strtolower(preg_replace('/[^a-z0-9]+/i', '_', $slug)); $slug = trim($slug, '_'); // Ensure slug uniqueness (in case collision after normalization) $slug = $this->ensureUniqueSlug($slug); $data = [ 'name' => $name, 'slug' => $slug, 'description' => $description, 'dashboard_route' => $dashboard_route, 'priority' => $priority, 'is_active' => $isActive, 'created_at' => utc_now(), 'updated_at' => utc_now(), ]; if ($this->roleModel->insert($data)) { return redirect()->to('rolepermission/roles')->with('success', 'Role created successfully!'); } return redirect()->back()->withInput()->with('error', 'Failed to create role.'); } /** * Ensure slug is unique in roles.slug; if exists, append -2, -3, ... */ /** * Ensure slug is unique in the roles table. * - If $ignoreId is provided, that record is excluded (for updates). * - Appends _2, _3, ... if duplicates exist. */ private function ensureUniqueSlug(string $baseSlug, ?int $ignoreId = null): string { $slug = $baseSlug; $i = 2; while (true) { $builder = $this->roleModel->where('slug', $slug); if ($ignoreId !== null) { $builder->where('id !=', $ignoreId); } $exists = $builder->countAllResults() > 0; if (!$exists) { break; } $slug = $baseSlug . '_' . $i; $i++; if ($i > 100000) { // safety stop break; } } return $slug; } public function editRole($id) { if (!$role = $this->roleModel->find($id)) { return redirect()->to('rolepermission/roles')->with('error', 'Role not found.'); } return view('rolepermission/edit_role', ['role' => $role]); } public function updateRole($id) { $role = $this->roleModel->find($id); if (!$role) { return redirect()->to('rolepermission/roles')->with('error', 'Role not found.'); } $validation = \Config\Services::validation(); // CI4 is_unique format: is_unique[table.field,ignore_field,ignore_value] $validation->setRules([ 'name' => "required|min_length[3]|max_length[255]|is_unique[roles.name,id,{$id}]", 'slug' => "permit_empty|max_length[64]|is_unique[roles.slug,id,{$id}]", 'description' => 'permit_empty|max_length[500]', 'dashboard_route' => [ 'rules' => 'required|min_length[1]|max_length[255]|regex_match[/^[a-z0-9_\\/\\-]+$/]', 'errors' => [ 'regex_match' => 'Route may contain lowercase letters, numbers, /, -, and _.' ] ], 'priority' => 'required|integer|greater_than_equal_to[0]|less_than_equal_to[100000]', 'is_active' => 'required|in_list[0,1]', ]); if (!$validation->withRequest($this->request)->run()) { return redirect()->back()->withInput()->with('errors', $validation->getErrors()); } // Normalize inputs (lowercase storage) $rawName = trim((string) $this->request->getPost('name')); $rawSlug = trim((string) $this->request->getPost('slug')); $route = trim((string) $this->request->getPost('dashboard_route')); $desc = trim((string) $this->request->getPost('description')); $priority = (int) $this->request->getPost('priority'); $isActive = (string) $this->request->getPost('is_active') === '1' ? 1 : 0; $name = strtolower($rawName); $dashboard_route = strtolower($route); $description = strtolower($desc); // slug: if blank, derive from name; else normalize $slug = $rawSlug !== '' ? $rawSlug : $name; $slug = strtolower(preg_replace('/[^a-z0-9]+/i', '_', $slug)); $slug = trim($slug, '_'); // Ensure unique slug if changed or blank turned into new value if ($slug !== $role['slug']) { $slug = $this->ensureUniqueSlug($slug, (int) $id); } $data = [ 'name' => $name, 'slug' => $slug, 'description' => $description, 'dashboard_route' => $dashboard_route, 'priority' => $priority, 'is_active' => $isActive, 'updated_at' => utc_now(), ]; if ($this->roleModel->update($id, $data)) { return redirect()->to('rolepermission/roles')->with('success', 'Role updated successfully!'); } return redirect()->back()->withInput()->with('error', 'Failed to update role.'); } public function deleteRole($id) { // Check if role exists $role = $this->roleModel->find($id); if (!$role) { return redirect()->to(site_url('rolepermission/roles')) ->with('error', 'Role not found.'); } // Try deleting if ($this->roleModel->delete($id)) { return redirect()->to(site_url('rolepermission/roles')) ->with('success', 'Role deleted successfully.'); } else { return redirect()->to(site_url('rolepermission/roles')) ->with('error', 'Failed to delete role.'); } } // ADD: show form + handle submit public function addPermission() { if (strtolower($this->request->getMethod()) === 'post') { $name = trim((string) $this->request->getPost('name')); $desc = trim((string) $this->request->getPost('description')); // Basic guard if ($name === '') { return redirect()->back() ->with('error', 'Permission name is required.') ->withInput(); } // Check existence (collation is case-insensitive: utf8mb4_unicode_ci) $existing = $this->permissionModel->where('name', $name)->first(); if ($existing) { // Idempotent: do nothing if already exists return redirect()->to(site_url('rolepermission/list_permissions')) ->with('success', 'Permission already exists. No changes made.'); } // Create new $data = [ 'name' => $name, 'description' => $desc, ]; if (!$this->permissionModel->save($data)) { return redirect()->back() ->with('error', implode(' ', $this->permissionModel->errors())) ->withInput(); } return redirect()->to(site_url('rolepermission/list_permissions')) ->with('success', 'Permission added successfully.'); } // GET -> form return view('rolepermission/add_permission'); } // EDIT: show form + handle submit public function editPermission($id = null) { $permission = $this->permissionModel->find($id); if (!$permission) { return redirect()->to(site_url('roles'))->with('error', 'Permission not found.'); } if (strtolower($this->request->getMethod()) === 'post') { $data = [ 'id' => $id, // needed for is_unique ignore 'name' => trim($this->request->getPost('name')), 'description' => trim($this->request->getPost('description')), ]; if (!$this->permissionModel->save($data)) { return redirect()->back() ->with('error', implode(' ', $this->permissionModel->errors())) ->withInput(); } return redirect()->to(site_url('rolepermission/list_permissions')) ->with('success', 'Permission updated successfully.'); } return view('rolepermission/edit_permission', ['permission' => $permission]); } // Optional delete public function deletePermission($id = null) { if (!$this->permissionModel->find($id)) { return redirect()->to(site_url('rolepermission/list_permissions'))->with('error', 'Permission not found.'); } $this->permissionModel->delete($id); return redirect()->to(site_url('rolepermission/list_permissions'))->with('success', 'Permission deleted.'); } public function listPermissions() { helper('url'); return view('rolepermission/permissionList', [ 'permissionsEndpoint' => site_url('api/rolepermission/permissions'), ]); } public function permissionsData() { $rows = $this->permissionModel ->select('id, name, description, created_at, updated_at') ->orderBy('id', 'ASC') ->findAll(); $normalized = array_map(static function ($row) { if (!is_array($row)) { return []; } return [ 'id' => (int) ($row['id'] ?? 0), 'name' => (string) ($row['name'] ?? ''), 'description' => $row['description'] ?? null, 'created_at' => $row['created_at'] ?? null, 'updated_at' => $row['updated_at'] ?? null, ]; }, $rows ?? []); return $this->response->setJSON(['permissions' => $normalized]); } }