diff --git a/app/Controllers/DashboardController.php b/app/Controllers/DashboardController.php index 4f3a69b..8d49cc3 100644 --- a/app/Controllers/DashboardController.php +++ b/app/Controllers/DashboardController.php @@ -57,6 +57,37 @@ class DashboardController extends Controller $formattedRecentDomains = \App\Helpers\DomainHelper::formatMultiple($recentDomains); $formattedExpiringDomains = \App\Helpers\DomainHelper::formatMultiple($expiringThisMonth); + // Get all domains for registrar distribution & notification coverage + if ($isolationMode === 'isolated') { + $allDomains = $this->domainModel->getAllWithGroups($userId); + } else { + $allDomains = $this->domainModel->getAllWithGroups(); + } + + // Registrar distribution + $registrarCounts = []; + foreach ($allDomains as $d) { + $reg = !empty($d['registrar']) ? $d['registrar'] : 'Unknown'; + $registrarCounts[$reg] = ($registrarCounts[$reg] ?? 0) + 1; + } + arsort($registrarCounts); + + // Notification coverage + $domainsWithGroup = count(array_filter($allDomains, fn($d) => !empty($d['group_name']))); + $totalDomainCount = count($allDomains); + + // Total channels + $totalChannels = 0; + foreach ($groups as $g) { $totalChannels += ($g['channel_count'] ?? 0); } + + // Get user's tags with usage + $tagModel = new \App\Models\Tag(); + if ($isolationMode === 'isolated') { + $dashTags = $tagModel->getAllWithUsage($userId); + } else { + $dashTags = $tagModel->getAllWithUsage(); + } + $this->view('dashboard/index', [ 'recentDomains' => $formattedRecentDomains, 'expiringThisMonth' => $formattedExpiringDomains, @@ -64,6 +95,11 @@ class DashboardController extends Controller 'recentLogs' => $recentLogs, 'groups' => $groups, 'systemStatus' => $systemStatus, + 'registrarCounts' => $registrarCounts, + 'domainsWithGroup' => $domainsWithGroup, + 'totalDomainCount' => $totalDomainCount, + 'totalChannels' => $totalChannels, + 'dashTags' => $dashTags, 'title' => 'Dashboard' ]); } diff --git a/app/Controllers/DomainController.php b/app/Controllers/DomainController.php index 23a8d5d..bde5cfc 100644 --- a/app/Controllers/DomainController.php +++ b/app/Controllers/DomainController.php @@ -531,7 +531,7 @@ class DomainController extends Controller } if (!$domain) { - $_SESSION['error'] = 'Domain not found'; + $_SESSION['error'] = 'You do not have permission to view this domain.'; $this->redirect('/domains'); return; } diff --git a/app/Controllers/TagController.php b/app/Controllers/TagController.php index f0a9ff0..51a60c2 100644 --- a/app/Controllers/TagController.php +++ b/app/Controllers/TagController.php @@ -250,6 +250,13 @@ class TagController extends Controller $userId = \Core\Auth::id(); $settingModel = new \App\Models\Setting(); $isolationMode = $settingModel->getValue('user_isolation_mode', 'shared'); + + // Check if user can access this tag in isolation mode + if ($isolationMode === 'isolated' && !$this->tagModel->canUserAccessTag($id, $userId, true)) { + $_SESSION['error'] = 'You do not have permission to view this tag.'; + $this->redirect('/tags'); + return; + } // Get domains for this tag with proper formatting $domainModel = new \App\Models\Domain(); diff --git a/app/Controllers/UserController.php b/app/Controllers/UserController.php index 1f5815e..a3a3f31 100644 --- a/app/Controllers/UserController.php +++ b/app/Controllers/UserController.php @@ -195,6 +195,62 @@ class UserController extends Controller } } + /** + * Show user profile view (admin) + */ + public function show($params = []) + { + $userId = $params['id'] ?? 0; + $user = $this->userModel->find($userId); + + if (!$user) { + $_SESSION['error'] = 'User not found'; + $this->redirect('/users'); + return; + } + + // Get user's domains (formatted for display) + $domainModel = new \App\Models\Domain(); + $domains = $domainModel->getAllWithGroups($userId); + $domains = \App\Helpers\DomainHelper::formatMultiple($domains); + $userDomainStats = $domainModel->getStatistics($userId); + + // Get user's tags with domains per tag + $tagModel = new \App\Models\Tag(); + $tags = $tagModel->getAllWithUsage($userId); + + // Fetch domains for each tag (formatted for display) + foreach ($tags as &$tag) { + $tagDomains = $tagModel->getDomainsForTag($tag['id'], $userId); + $tag['domains'] = \App\Helpers\DomainHelper::formatMultiple($tagDomains); + } + unset($tag); + + // Get user's notification groups with channels + $groupModel = new \App\Models\NotificationGroup(); + $groups = $groupModel->getAllWithChannelCount($userId); + + // Fetch channels for each group + $channelModel = new \App\Models\NotificationChannel(); + foreach ($groups as &$group) { + $group['channels'] = $channelModel->getByGroupId($group['id']); + } + unset($group); + + // Get 2FA status + $twoFactorStatus = $this->userModel->getTwoFactorStatus($userId); + + $this->view('users/show', [ + 'title' => htmlspecialchars($user['full_name']) . ' - User Profile', + 'user' => $user, + 'domains' => $domains, + 'userDomainStats' => $userDomainStats, + 'tags' => $tags, + 'groups' => $groups, + 'twoFactorStatus' => $twoFactorStatus, + ]); + } + /** * Show edit user form */ diff --git a/app/Views/dashboard/index.php b/app/Views/dashboard/index.php index 4f575ab..4af6826 100644 --- a/app/Views/dashboard/index.php +++ b/app/Views/dashboard/index.php @@ -9,12 +9,62 @@ if (!isset($domainStats)) { $domainStats = \App\Helpers\LayoutHelper::getDomainStats(); } +// Prepare widget data +$topRegistrars = array_slice($registrarCounts ?? [], 0, 8, true); +$topTags = array_slice(array_filter($dashTags ?? [], fn($t) => ($t['usage_count'] ?? 0) > 0), 0, 8); +$domainsWithoutGroup = ($totalDomainCount ?? 0) - ($domainsWithGroup ?? 0); +$totalGroupCount = count($groups ?? []); + ob_start(); ?> + + +
+
+ + + System Status + + 'text-green-600', + 'yellow' => 'text-yellow-600', + 'red' => 'text-red-600', + 'gray' => 'text-gray-600' + ]; + $statusDots = [ + 'green' => 'bg-green-500', + 'yellow' => 'bg-yellow-500', + 'red' => 'bg-red-500', + 'gray' => 'bg-gray-400' + ]; + ?> +
+ + + Database + + + | + + + TLD Registry + + + | + + + Notifications + + +
+
+
+ + -
- +
@@ -26,8 +76,6 @@ ob_start();
- -
@@ -39,8 +87,6 @@ ob_start();
- -
@@ -53,8 +99,6 @@ ob_start();
- -
@@ -68,18 +112,22 @@ ob_start();
- -
+ +
-
-
-
+
+
+

Recent Domains

+ + View all +
-
+
+
@@ -93,11 +141,7 @@ ob_start();
- - - - Not set - + @@ -109,13 +153,8 @@ ob_start();
-
@@ -140,128 +173,168 @@ ob_start();
-
- -
- -
-
+ +
+
+

- - Quick Actions + + Expiring Soon

+ 5): ?> + + View all + + +
+
+ -
- - -
-
-

- - System Status -

-
-
- 'text-green-600', - 'yellow' => 'text-yellow-600', - 'red' => 'text-red-600', - 'gray' => 'text-gray-600' - ]; - ?> -
- Database - - - - -
-
- TLD Registry - - - - -
-
- Notifications - - - - -
-
-
- - -
-
-
-

- - Expiring Soon -

- 5): ?> - - View all - + + + +
+
- -
- - -
-
-

-

- - - days - -

+ +
+ +

No domains expiring soon

+

within days

+
+ +
+
+ + +
+ +
+
+

+ + Registrar Distribution +

+ registrar +
+
+ +
+ $regCount): ?> + 0 ? round(($regCount / $totalDomainCount) * 100) : 0; ?> +
+
+ + (%) +
+
+
- - -
-
- -

No domains expiring soon

-

within days

+
+ +

No registrar data

+
+ +
+
+ + +
+
+

+ + Tag Usage +

+ tag +
+
+ +
+ + 0 ? round(($tt['usage_count'] / $totalDomainCount) * 100) : 0; ?> +
+
+ + + + + domain (%) +
+
+
+
+
+ +
+ +
+ +

No tags in use

+
+ +
+
+ + +
+
+

+ + Notification Coverage +

+ group, channel +
+
+ 0): ?> + +
+
+ + + + +
+ % +
+
+
+
+
+

+

With Notifications

+
+
+

+

Without Notifications

+
+
+ +
+ +

No domains to monitor

diff --git a/app/Views/layout/base.php b/app/Views/layout/base.php index 4dfed8a..51764dc 100644 --- a/app/Views/layout/base.php +++ b/app/Views/layout/base.php @@ -240,47 +240,49 @@ if (!isset($appName)) { } }); + // Close all dropdowns except the one specified + function closeOtherDropdowns(exceptId) { + ['userDropdown', 'notificationsDropdown', 'quickActionsDropdown'].forEach(id => { + if (id !== exceptId) { + const el = document.getElementById(id); + if (el) el.classList.remove('show'); + } + }); + } + // Toggle user dropdown function toggleDropdown() { - const dropdown = document.getElementById('userDropdown'); - const notifDropdown = document.getElementById('notificationsDropdown'); - - // Close notifications dropdown if open - if (notifDropdown && notifDropdown.classList.contains('show')) { - notifDropdown.classList.remove('show'); - } - - dropdown.classList.toggle('show'); + closeOtherDropdowns('userDropdown'); + document.getElementById('userDropdown').classList.toggle('show'); } // Toggle notifications dropdown function toggleNotifications() { - const dropdown = document.getElementById('notificationsDropdown'); - const userDropdown = document.getElementById('userDropdown'); - - // Close user dropdown if open - if (userDropdown && userDropdown.classList.contains('show')) { - userDropdown.classList.remove('show'); - } - - dropdown.classList.toggle('show'); + closeOtherDropdowns('notificationsDropdown'); + document.getElementById('notificationsDropdown').classList.toggle('show'); + } + + // Toggle quick actions dropdown + function toggleQuickActions() { + closeOtherDropdowns('quickActionsDropdown'); + document.getElementById('quickActionsDropdown').classList.toggle('show'); } // Close dropdowns when clicking outside document.addEventListener('click', function(event) { - const userDropdown = document.getElementById('userDropdown'); - const notifDropdown = document.getElementById('notificationsDropdown'); + const dropdowns = [ + { id: 'userDropdown', trigger: '[onclick="toggleDropdown()"]' }, + { id: 'notificationsDropdown', trigger: '[onclick="toggleNotifications()"]' }, + { id: 'quickActionsDropdown', trigger: '[onclick="toggleQuickActions()"]' } + ]; - const isUserDropdownClick = event.target.closest('[onclick="toggleDropdown()"]') || event.target.closest('#userDropdown'); - const isNotifDropdownClick = event.target.closest('[onclick="toggleNotifications()"]') || event.target.closest('#notificationsDropdown'); - - if (!isUserDropdownClick && userDropdown && userDropdown.classList.contains('show')) { - userDropdown.classList.remove('show'); - } - - if (!isNotifDropdownClick && notifDropdown && notifDropdown.classList.contains('show')) { - notifDropdown.classList.remove('show'); - } + dropdowns.forEach(({ id, trigger }) => { + const dd = document.getElementById(id); + if (dd && dd.classList.contains('show')) { + const isInside = event.target.closest(trigger) || event.target.closest('#' + id); + if (!isInside) dd.classList.remove('show'); + } + }); }); // Live Search Functionality diff --git a/app/Views/layout/top-nav.php b/app/Views/layout/top-nav.php index f28ea93..63dc9d9 100644 --- a/app/Views/layout/top-nav.php +++ b/app/Views/layout/top-nav.php @@ -50,10 +50,41 @@
- - - - + +
@@ -94,7 +125,7 @@
-
+ diff --git a/app/Views/users/index.php b/app/Views/users/index.php index 458994b..c8550cd 100644 --- a/app/Views/users/index.php +++ b/app/Views/users/index.php @@ -280,6 +280,9 @@ $pagination = $pagination ?? [
+ + + diff --git a/app/Views/users/show.php b/app/Views/users/show.php new file mode 100644 index 0000000..84b5c1b --- /dev/null +++ b/app/Views/users/show.php @@ -0,0 +1,1090 @@ + + + +
+ + + Back to Users + + +
+ + + Edit User + + + + + + + + + + +
+ + +
+ +
+
+ + +
+
+ +
+ + <?= htmlspecialchars($avatar['alt']) ?> + + + + + +
+ + +
+
+

+ + + + Admin + + + + User + + + + + + Active + + + + Inactive + + +
+ +
+
+ + + + + 2FA + + + + No 2FA + + +
+
+ + + + + + + +
+
+ +
+
+ + Member since +
+
+ + Last login: +
+
+
+ + +
+
+

+

Domains

+
+
+

+

Tags

+
+
+

+

Groups

+
+
+
+
+ + +
+
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + + diff --git a/routes/web.php b/routes/web.php index 43ea567..7c846f6 100644 --- a/routes/web.php +++ b/routes/web.php @@ -163,6 +163,7 @@ $router->get('/api/notifications/recent', [NotificationController::class, 'getRe $router->get('/users', [UserController::class, 'index']); $router->get('/users/create', [UserController::class, 'create']); $router->post('/users/store', [UserController::class, 'store']); +$router->get('/users/{id}', [UserController::class, 'show']); $router->get('/users/{id}/edit', [UserController::class, 'edit']); $router->post('/users/{id}/update', [UserController::class, 'update']); $router->post('/users/{id}/delete', [UserController::class, 'delete']);