getCurrentUser(); if (!$user) { return new Response('', 302, ['Location' => url('/login')]); } // Get enabled modules for the user's company $enabledModules = $this->getEnabledModules($user); // Get widgets from authorized modules $widgets = $this->getAuthorizedWidgets($enabledModules, $user); // Get system statistics $stats = $this->getSystemStats($user); // Sidebar and common data are handled by renderAdminView, // but since dashboard/index.twig expects specific variables, // we can still pass them explicitly if we use the old way, // OR we can use renderAdminView and just pass the extra data. // Check if SEO module is installed but not configured $seoPending = false; try { $seoModule = Module::where('modules_name', 'seo')->active()->first(); if ($seoModule && empty($seoModule->modules_settings)) { $seoPending = true; } } catch (\Throwable $e) { // Silently fail if something goes wrong with SEO check error_log("SEO Check Error: " . $e->getMessage()); } // Using renderAdminView for consistency: return $this->renderAdminView('dashboard/index.twig', [ 'stats' => $stats, 'recent_activity' => $this->getRecentActivity($user), 'widgets' => $widgets, 'seo_pending' => $seoPending, 'cache_cleared' => $request->query->get('cache_cleared'), 'deleted_count' => $request->query->get('deleted'), 'opcache_cleared' => $request->query->get('opcache') ]); } /** * Get authorized widgets for the user. * * @param \Illuminate\Database\Eloquent\Collection $modules * @param User $user * @return array */ protected function getAuthorizedWidgets($modules, User $user): array { $widgets = []; foreach ($modules as $module) { if (!$this->userHasModulePermission($user, $module)) { continue; } $moduleNamespace = $module->modules_namespace; // If namespace is not defined, try to guess it from name (fallback) if (!$moduleNamespace) { $moduleNamespace = "Modules\\" . ucfirst($module->modules_name); } // Ensure namespace starts with \Modules\ if (strpos($moduleNamespace, 'Modules\\') !== 0 && strpos($moduleNamespace, '\\Modules\\') !== 0) { $moduleNamespace = "Modules\\" . ltrim($moduleNamespace, '\\'); } $widgetClass = (strpos($moduleNamespace, '\\') === 0 ? "" : "\\") . $moduleNamespace . "\\Widgets\\DashboardWidget"; if (class_exists($widgetClass)) { try { $widget = new $widgetClass(); // Check if user can view this widget if ($this->userCanViewWidget($user, $widget)) { $widgets[] = $widget->render(); } } catch (\Throwable $e) { error_log("Error loading widget {$widgetClass}: " . $e->getMessage()); } } } return $widgets; } /** * Check if user can view a widget. * * @param User $user * @param mixed $widget * @return bool */ protected function userCanViewWidget(User $user, $widget): bool { // Check if widget defines required permissions if (method_exists($widget, 'getRequiredPermission')) { $requiredPermission = $widget->getRequiredPermission(); return $user->hasPermission($requiredPermission); } // If no specific permission, check module access return true; } /** * Get system statistics. * * @param User $user * @return array */ protected function getSystemStats(User $user): array { try { if ($user->isSuperAdmin()) { return $this->getSuperAdminStats(); } return $this->getCompanyStats($user); } catch (\Exception $e) { error_log("Error loading system stats: " . $e->getMessage()); return [ 'total_companies' => 0, 'active_companies' => 0, 'total_users' => 0, 'active_users' => 0, 'total_modules' => 0, 'active_modules' => 0, 'user_count' => 0, 'enabled_modules' => 0 ]; } } /** * Get statistics for Super Admin. * * @return array */ protected function getSuperAdminStats(): array { return [ 'total_companies' => class_exists('Modules\Company\Models\Company') ? \Modules\Company\Models\Company::count() : 0, 'active_companies' => class_exists('Modules\Company\Models\Company') ? \Modules\Company\Models\Company::active()->count() : 0, 'total_users' => User::count(), 'active_users' => User::active()->count(), 'total_modules' => Module::count(), 'active_modules' => Module::active()->count() ]; } /** * Get statistics for company users. * * @param User $user * @return array */ protected function getCompanyStats(User $user): array { $companyId = $user->users_bus_id; return [ 'user_count' => User::where('users_bus_id', $companyId)->count(), 'active_users' => User::where('users_bus_id', $companyId)->active()->count(), 'enabled_modules' => DB::table('bus_modules') ->where('bus_modules_bus_id', $companyId) ->where('bus_modules_is_enabled', true) ->count() ]; } /** * Get recent activity for the user. * * @param User $user * @return \Illuminate\Database\Eloquent\Collection */ protected function getRecentActivity(User $user) { try { if (!class_exists('App\Models\ActivityLog')) { return collect([]); } return \App\Models\ActivityLog::with('user') ->forUser($user->users_id) ->orderByDate('desc') ->limit(10) ->get(); } catch (\Exception $e) { error_log("Error loading recent activity: " . $e->getMessage()); return collect([]); } } /** * Show module installation form. * * @param Request $request * @return Response */ public function showInstallModule(Request $request): Response { $user = $this->getCurrentUser(); if (!$user || !$user->isSuperAdmin()) { return new Response('', 302, ['Location' => url('/dashboard')]); } $success = isset($_SESSION['flash_success']) ? (array) $_SESSION['flash_success'] : []; $error = isset($_SESSION['flash_error']) ? (array) $_SESSION['flash_error'] : []; unset($_SESSION['flash_success'], $_SESSION['flash_error']); return $this->renderAdminView('modules/install.twig', [ 'error' => $error, 'success' => $success ]); } /** * Install a module from zip file. * * @param Request $request * @return Response */ public function installModule(Request $request): Response { $user = $this->getCurrentUser(); if (!$user || !$user->isSuperAdmin()) { return new Response('', 302, ['Location' => url('/dashboard')]); } try { // Validate uploaded file if (!$request->files->has('module_zip')) { throw new \Exception(__('modules.error_no_file')); } $file = $request->files->get('module_zip'); // Validate file type if ($file->getMimeType() !== 'application/zip' && $file->getMimeType() !== 'application/x-zip-compressed') { throw new \Exception(__('modules.error_invalid_zip')); } // Create temporary directory $tempDir = sys_get_temp_dir() . '/module_install_' . time(); mkdir($tempDir, 0755, true); // Extract zip file $zip = new \ZipArchive(); if ($zip->open($file->getPathname()) !== TRUE) { throw new \Exception(__('modules.error_open_zip')); } if (!$zip->extractTo($tempDir)) { throw new \Exception(__('modules.error_extract_zip')); } $zip->close(); // Validate module structure $this->validateModuleStructure($tempDir); // Read module configuration $moduleConfig = $this->readModuleConfig($tempDir); // Check if module already exists if (Module::where('modules_name', $moduleConfig['name'])->exists()) { throw new \Exception(__('modules.error_module_exists')); } // Copy module files to modules directory $modulesPath = dirname(__DIR__, 2) . '/modules/' . $moduleConfig['name']; $this->recursiveCopy($tempDir, $modulesPath); // Run migrations if they exist $this->runModuleMigrations($modulesPath); // Check if module was registered by migrations $module = Module::where('modules_name', $moduleConfig['name'])->first(); if (!$module) { // Register module in database $module = $this->registerModule($moduleConfig); } // Enable module for all companies if specified if ($request->request->get('enable_for_all', false)) { $this->enableModuleForAllCompanies($module->modules_id, $user->users_id); } // Clean up $this->recursiveDelete($tempDir); $_SESSION['flash_success'][] = __('modules.success_install'); return new Response('', 302, ['Location' => url('/admin/modules/install')]); } catch (\Throwable $e) { // Clean up on error if (isset($tempDir) && is_dir($tempDir)) { $this->recursiveDelete($tempDir); } $_SESSION['flash_error'][] = $e->getMessage(); return new Response('', 302, ['Location' => url('/admin/modules/install')]); } } /** * Validate module structure. * * @param string $modulePath * @throws \Exception */ protected function validateModuleStructure(string $modulePath): void { $requiredFiles = ['module.json']; foreach ($requiredFiles as $file) { if (!file_exists($modulePath . '/' . $file)) { throw new \Exception(__('modules.error_missing_file', ['file' => $file])); } } } /** * Read module configuration. * * @param string $modulePath * @return array * @throws \Exception */ protected function readModuleConfig(string $modulePath): array { $configPath = $modulePath . '/module.json'; if (!file_exists($configPath)) { throw new \Exception(__('modules.error_missing_config')); } $config = json_decode(file_get_contents($configPath), true); if (json_last_error() !== JSON_ERROR_NONE) { throw new \Exception(__('modules.error_invalid_config', ['error' => json_last_error_msg()])); } $requiredFields = ['name', 'version', 'description']; foreach ($requiredFields as $field) { if (empty($config[$field])) { throw new \Exception(__('modules.error_missing_field', ['field' => $field])); } } return $config; } /** * Recursively copy directory. * * @param string $source * @param string $destination */ protected function recursiveCopy(string $source, string $destination): void { if (!is_dir($destination)) { mkdir($destination, 0755, true); } $iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST ); foreach ($iterator as $item) { // Calculate subPathName manually to ensure compatibility $len = strlen($source); $subPathName = substr($item->getPathname(), $len + 1); $targetPath = $destination . '/' . $subPathName; if ($item->isDir()) { mkdir($targetPath, 0755, true); } else { copy($item, $targetPath); } } } /** * Recursively delete directory. * * @param string $dir */ protected function recursiveDelete(string $dir): void { if (!is_dir($dir)) { return; } $files = array_diff(scandir($dir), ['.', '..']); foreach ($files as $file) { $path = $dir . '/' . $file; if (is_dir($path)) { $this->recursiveDelete($path); } else { unlink($path); } } rmdir($dir); } /** * Run module migrations. * * @param string $modulePath */ protected function runModuleMigrations(string $modulePath): void { $migrationsPath = $modulePath . '/database/migrations'; if (!is_dir($migrationsPath)) { return; } $migrations = glob($migrationsPath . '/*.php'); foreach ($migrations as $migration) { require_once $migration; // Try to find class name from file content $content = file_get_contents($migration); if (preg_match('/class\s+(\w+)/', $content, $matches)) { $className = $matches[1]; if (class_exists($className)) { $instance = new $className(); if (method_exists($instance, 'up')) { try { $instance->up(); } catch (\Exception $e) { error_log("Migration failed for $className: " . $e->getMessage()); throw $e; // Re-throw to show error to user } } } else { error_log("Class $className not found in migration file: $migration"); } } else { error_log("Could not find class name in migration file: $migration"); } } } /** * Fix module menus in database. * * @return Response */ public function fixModuleMenus(): Response { $user = $this->getCurrentUser(); if (!$user || !$user->isSuperAdmin()) { return new Response('Access denied', 403); } $modules = Module::all(); $count = 0; foreach ($modules as $module) { $modulePath = dirname(__DIR__, 2) . '/' . $module->modules_path; try { $config = $this->readModuleConfig($modulePath); // Update module settings with menu $module->modules_settings = array_merge($module->modules_settings ?? [], $config['settings'] ?? [], ['menu' => $config['menu'] ?? []]); // Also update top-level module fields that might be in module.json if (isset($config['display_name'])) $module->modules_display_name = $config['display_name']; if (isset($config['description'])) $module->modules_description = $config['description']; if (isset($config['version'])) $module->modules_version = $config['version']; if (isset($config['icon'])) $module->modules_icon = $config['icon']; if (isset($config['menu_side_bar'])) $module->menu_side_bar = (bool)$config['menu_side_bar']; if (isset($config['menu_nav_bar'])) $module->menu_nav_bar = (bool)$config['menu_nav_bar']; if (isset($config['order'])) $module->modules_order = (int)$config['order']; if (isset($config['namespace'])) $module->modules_namespace = $config['namespace']; $module->save(); $count++; } catch (\Exception $e) { // Ignore errors for individual modules error_log("Failed to update menu for module {$module->modules_name}: " . $e->getMessage()); } } $_SESSION['flash_success'][] = __('modules.success_menus_updated', ['count' => $count]); return new Response('', 302, ['Location' => url('/admin/modules/install')]); } /** * Register module in database. * * @param array $config * @return Module */ protected function registerModule(array $config): Module { return Module::create([ 'modules_name' => $config['name'], 'modules_display_name' => $config['display_name'] ?? $config['name'], 'modules_description' => $config['description'], 'modules_version' => $config['version'], 'modules_author' => $config['author'] ?? 'Unknown', 'modules_ative' => true, 'modules_public' => $config['public'] ?? false, 'modules_blocked' => false, 'modules_dependencies' => $config['dependencies'] ?? [], 'modules_permissions' => $config['permissions'] ?? [], 'modules_settings' => array_merge($config['settings'] ?? [], ['menu' => $config['menu'] ?? []]), 'modules_path' => 'modules/' . $config['name'], 'modules_namespace' => $config['namespace'] ?? ('Modules\\' . ucfirst($config['name'])), 'modules_icon' => $config['icon'] ?? 'bi bi-gear', 'menu_side_bar' => $config['menu_side_bar'] ?? true, 'menu_nav_bar' => $config['menu_nav_bar'] ?? false, 'modules_order' => $config['order'] ?? 999, 'modules_install_date' => \Carbon\Carbon::now(), 'modules_update_date' => \Carbon\Carbon::now() ]); } /** * Enable module for all companies. * * @param int $moduleId * @param int $enabledBy */ protected function enableModuleForAllCompanies(int $moduleId, int $enabledBy): void { $companies = Company::all(); foreach ($companies as $company) { $company->enableModule($moduleId, $enabledBy); } } /** * Search for modules and users. * * @param Request $request * @return Response */ public function search(Request $request): Response { $user = $this->getCurrentUser(); $query = $request->query->get('q'); if (!$user) { return new Response('', 302, ['Location' => url('/login')]); } if (empty($query)) { return $this->renderAdminView('dashboard/search_results.twig', [ 'query' => '', 'results' => ['modules' => [], 'users' => []] ]); } $results = [ 'modules' => [], 'users' => [] ]; // Search Modules $results['modules'] = Module::where('modules_name', 'LIKE', "%{$query}%") ->orWhere('modules_display_name', 'LIKE', "%{$query}%") ->orWhere('modules_description', 'LIKE', "%{$query}%") ->get(); // Search Users (Super Admin or Company Admin) if ($user->isSuperAdmin()) { $results['users'] = User::where('users_name', 'LIKE', "%{$query}%") ->orWhere('users_email', 'LIKE', "%{$query}%") ->get(); } elseif ($user->users_bus_id) { $results['users'] = User::where('users_bus_id', $user->users_bus_id) ->where(function($q) use ($query) { $q->where('users_name', 'LIKE', "%{$query}%") ->orWhere('users_email', 'LIKE', "%{$query}%"); }) ->get(); } return $this->renderAdminView('dashboard/search_results.twig', [ 'query' => $query, 'results' => $results ]); } }