feat: Implement unified sidebar with Nuxt UI across all dashboard pages
- Install @nuxt/ui and integrate with existing Vuetify - Create new layouts/dashboard-unified.vue with modern sidebar design - Features: clean white design, collapsible sidebar, role-based navigation - Remove old layouts/dashboard.vue to eliminate dual-sidebar confusion - Update all dashboard pages to use dashboard-unified layout - Add demo page showcasing new sidebar features - Fix auth error handler to ignore external service 401 errors - Ensure consistent navigation experience across entire platform
This commit is contained in:
@@ -1,293 +1,13 @@
|
||||
<template>
|
||||
<v-app full-height>
|
||||
<v-navigation-drawer
|
||||
v-model="drawer"
|
||||
:location="mdAndDown ? 'bottom' : undefined"
|
||||
>
|
||||
<v-img v-if="!mdAndDown" src="/Port_Nimara_Logo_2_Colour_New_Transparent.png" height="110" class="my-6" contain />
|
||||
|
||||
<v-list color="primary" lines="two">
|
||||
<v-list-item
|
||||
v-for="(item, index) in safeMenu"
|
||||
:key="index"
|
||||
:to="item.to"
|
||||
:title="item.title"
|
||||
:prepend-icon="item.icon"
|
||||
/>
|
||||
</v-list>
|
||||
|
||||
<template #append>
|
||||
<v-list lines="two">
|
||||
<v-list-item
|
||||
v-if="user"
|
||||
:title="user.name"
|
||||
:subtitle="user.email"
|
||||
prepend-icon="mdi-account"
|
||||
>
|
||||
<template #append>
|
||||
<v-chip v-if="user.tier && user.tier !== 'basic'" size="small" color="primary">
|
||||
{{ user.tier }}
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
@click="logOut"
|
||||
title="Log out"
|
||||
prepend-icon="mdi-logout"
|
||||
base-color="error"
|
||||
/>
|
||||
</v-list>
|
||||
</template>
|
||||
</v-navigation-drawer>
|
||||
|
||||
<v-app-bar v-if="mdAndDown" elevation="2">
|
||||
<template #prepend>
|
||||
<v-app-bar-nav-icon variant="text" @click.stop="drawer = !drawer" />
|
||||
</template>
|
||||
|
||||
<v-img src="/Port_Nimara_Logo_2_Colour_New_Transparent.png" height="50" />
|
||||
|
||||
<template #append>
|
||||
<v-btn
|
||||
@click="logOut"
|
||||
class="mr-3"
|
||||
variant="text"
|
||||
color="error"
|
||||
icon="mdi-logout"
|
||||
/>
|
||||
</template>
|
||||
</v-app-bar>
|
||||
|
||||
<v-main>
|
||||
<router-view />
|
||||
</v-main>
|
||||
</v-app>
|
||||
<div>
|
||||
<!-- This page now acts as a parent route for dashboard pages -->
|
||||
<NuxtPage />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
middleware: ["authentication"],
|
||||
layout: false,
|
||||
layout: "dashboard-unified",
|
||||
});
|
||||
|
||||
const { mdAndDown } = useDisplay();
|
||||
const { user, logout, authSource } = useUnifiedAuth();
|
||||
const { isAdmin, getUserGroups, getCurrentUser } = useAuthorization();
|
||||
const tags = usePortalTags();
|
||||
|
||||
const drawer = ref(false);
|
||||
|
||||
// Debug auth state
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
console.log('[Dashboard] Auth state on mount:', {
|
||||
isAdmin: isAdmin(),
|
||||
userGroups: getUserGroups(),
|
||||
currentUser: getCurrentUser()
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const interestMenu = computed(() => {
|
||||
const userIsAdmin = isAdmin();
|
||||
const userGroups = getUserGroups();
|
||||
|
||||
console.log('[Dashboard] Computing interest menu - isAdmin:', userIsAdmin, 'groups:', userGroups);
|
||||
|
||||
// Check if user has sales or admin privileges
|
||||
const hasSalesAccess = userGroups.includes('sales') || userGroups.includes('admin');
|
||||
|
||||
const baseMenu = [
|
||||
//{
|
||||
// to: "/dashboard/interest-eoi-queue",
|
||||
// icon: "mdi-tray-full",
|
||||
// title: "EOI Queue",
|
||||
//},
|
||||
{
|
||||
to: "/dashboard/interest-analytics",
|
||||
icon: "mdi-view-dashboard",
|
||||
title: "Analytics",
|
||||
},
|
||||
{
|
||||
to: "/dashboard/interest-berth-list",
|
||||
icon: "mdi-table",
|
||||
title: "Berth List",
|
||||
},
|
||||
{
|
||||
to: "/dashboard/interest-berth-status",
|
||||
icon: "mdi-sail-boat",
|
||||
title: "Berth Status",
|
||||
},
|
||||
{
|
||||
to: "/dashboard/interest-list",
|
||||
icon: "mdi-view-list",
|
||||
title: "Interest List",
|
||||
},
|
||||
{
|
||||
to: "/dashboard/interest-status",
|
||||
icon: "mdi-account-check",
|
||||
title: "Interest Status",
|
||||
},
|
||||
{
|
||||
to: "/dashboard/file-browser",
|
||||
icon: "mdi-folder",
|
||||
title: "File Browser",
|
||||
},
|
||||
];
|
||||
|
||||
// Only show expenses to sales and admin users
|
||||
if (hasSalesAccess) {
|
||||
console.log('[Dashboard] Adding expenses to menu (user has sales/admin access)');
|
||||
baseMenu.push({
|
||||
to: "/dashboard/expenses",
|
||||
icon: "mdi-receipt",
|
||||
title: "Expenses",
|
||||
});
|
||||
} else {
|
||||
console.log('[Dashboard] Hiding expenses from menu (user role:', userGroups, ')');
|
||||
}
|
||||
|
||||
// Add admin menu items if user is admin
|
||||
if (userIsAdmin) {
|
||||
console.log('[Dashboard] Adding admin console to interest menu');
|
||||
baseMenu.push({
|
||||
to: "/dashboard/admin",
|
||||
icon: "mdi-shield-crown",
|
||||
title: "Admin Console",
|
||||
});
|
||||
}
|
||||
|
||||
return baseMenu;
|
||||
});
|
||||
|
||||
const defaultMenu = computed(() => {
|
||||
const userIsAdmin = isAdmin();
|
||||
const userGroups = getUserGroups();
|
||||
|
||||
console.log('[Dashboard] Computing default menu - isAdmin:', userIsAdmin, 'groups:', userGroups);
|
||||
|
||||
const baseMenu = [
|
||||
{
|
||||
to: "/dashboard/site",
|
||||
icon: "mdi-view-dashboard",
|
||||
title: "Site Analytics",
|
||||
},
|
||||
{
|
||||
to: "/dashboard/data",
|
||||
icon: "mdi-finance",
|
||||
title: "Data Analytics",
|
||||
},
|
||||
{
|
||||
to: "/dashboard/file-browser",
|
||||
icon: "mdi-folder",
|
||||
title: "File Browser",
|
||||
},
|
||||
];
|
||||
|
||||
// Add admin menu items if user is admin
|
||||
if (userIsAdmin) {
|
||||
console.log('[Dashboard] Adding admin console to default menu');
|
||||
baseMenu.push({
|
||||
to: "/dashboard/admin",
|
||||
icon: "mdi-shield-crown",
|
||||
title: "Admin Console",
|
||||
});
|
||||
}
|
||||
|
||||
return baseMenu;
|
||||
});
|
||||
|
||||
const menu = computed(() => {
|
||||
try {
|
||||
const tagsValue = toValue(tags);
|
||||
const menuToUse = tagsValue.interest ? interestMenu.value : defaultMenu.value;
|
||||
|
||||
console.log('[Dashboard] Computing menu:', {
|
||||
hasInterestTag: tagsValue.interest,
|
||||
menuType: tagsValue.interest ? 'interestMenu' : 'defaultMenu',
|
||||
menuIsArray: Array.isArray(menuToUse),
|
||||
menuLength: menuToUse?.length
|
||||
});
|
||||
|
||||
return menuToUse;
|
||||
} catch (error) {
|
||||
console.error('[Dashboard] Error computing menu:', error);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
// Safe menu wrapper to prevent crashes when menu is undefined
|
||||
const safeMenu = computed(() => {
|
||||
try {
|
||||
const currentMenu = menu.value;
|
||||
if (Array.isArray(currentMenu)) {
|
||||
return currentMenu;
|
||||
}
|
||||
|
||||
console.warn('[Dashboard] Menu is not an array, returning fallback menu');
|
||||
|
||||
// Get current user permissions for fallback menu
|
||||
const userIsAdmin = isAdmin();
|
||||
const userGroups = getUserGroups();
|
||||
const hasSalesAccess = userGroups.includes('sales') || userGroups.includes('admin');
|
||||
|
||||
// Fallback menu with essential items (respecting permissions)
|
||||
const fallbackMenu = [
|
||||
{
|
||||
to: "/dashboard/interest-list",
|
||||
icon: "mdi-view-list",
|
||||
title: "Interest List",
|
||||
},
|
||||
{
|
||||
to: "/dashboard/file-browser",
|
||||
icon: "mdi-folder",
|
||||
title: "File Browser",
|
||||
},
|
||||
];
|
||||
|
||||
// Only add expenses if user has sales/admin access
|
||||
if (hasSalesAccess) {
|
||||
fallbackMenu.push({
|
||||
to: "/dashboard/expenses",
|
||||
icon: "mdi-receipt",
|
||||
title: "Expenses",
|
||||
});
|
||||
}
|
||||
|
||||
// Only add admin console if user is admin
|
||||
if (userIsAdmin) {
|
||||
fallbackMenu.push({
|
||||
to: "/dashboard/admin",
|
||||
icon: "mdi-shield-crown",
|
||||
title: "Admin Console",
|
||||
});
|
||||
}
|
||||
|
||||
return fallbackMenu;
|
||||
} catch (error) {
|
||||
console.error('[Dashboard] Error computing menu:', error);
|
||||
|
||||
// Emergency fallback menu - only essential items
|
||||
return [
|
||||
{
|
||||
to: "/dashboard/interest-list",
|
||||
icon: "mdi-view-list",
|
||||
title: "Interest List",
|
||||
},
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
const logOut = async () => {
|
||||
await logout();
|
||||
return navigateTo("/login");
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (mdAndDown.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
drawer.value = true;
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -238,6 +238,7 @@ import { formatDate, formatTime, formatDateTime } from '@/utils/dateUtils'
|
||||
|
||||
definePageMeta({
|
||||
middleware: ['authentication', 'authorization'],
|
||||
layout: 'dashboard-unified',
|
||||
auth: {
|
||||
roles: ['admin']
|
||||
}
|
||||
|
||||
@@ -230,6 +230,7 @@ import { formatTime, formatDateTime } from '@/utils/dateUtils'
|
||||
|
||||
definePageMeta({
|
||||
middleware: ['authentication', 'authorization'],
|
||||
layout: 'dashboard-unified',
|
||||
auth: {
|
||||
roles: ['admin']
|
||||
}
|
||||
|
||||
@@ -122,6 +122,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: 'dashboard-unified'
|
||||
});
|
||||
|
||||
const { user, isAuthenticated, authSource, isAdmin, logout } = useUnifiedAuth();
|
||||
const router = useRouter();
|
||||
|
||||
|
||||
@@ -347,7 +347,7 @@ const ExpenseCreateModal = defineAsyncComponent(() => import('@/components/Expen
|
||||
// Page meta
|
||||
definePageMeta({
|
||||
middleware: ['authentication', 'authorization'],
|
||||
layout: 'dashboard',
|
||||
layout: 'dashboard-unified',
|
||||
roles: ['sales', 'admin']
|
||||
});
|
||||
|
||||
|
||||
@@ -336,6 +336,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: 'dashboard-unified'
|
||||
});
|
||||
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import FileUploader from '~/components/FileUploader.vue';
|
||||
import FilePreviewModal from '~/components/FilePreviewModal.vue';
|
||||
|
||||
67
pages/dashboard/sidebar-demo.vue
Normal file
67
pages/dashboard/sidebar-demo.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-card>
|
||||
<v-card-title class="text-h4">
|
||||
New Unified Sidebar Demo
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-alert type="success" variant="tonal" class="mb-6">
|
||||
<div class="text-h6 mb-2">✨ Modern Sidebar Features</div>
|
||||
<ul class="pl-4">
|
||||
<li>Clean white design with subtle borders</li>
|
||||
<li>Collapsible sidebar with smooth animations</li>
|
||||
<li>Icons that change color when active</li>
|
||||
<li>User info with avatar at the bottom</li>
|
||||
<li>Role badges (Admin/Sales)</li>
|
||||
<li>Responsive - becomes a drawer on mobile</li>
|
||||
<li>Page title in the top navbar</li>
|
||||
</ul>
|
||||
</v-alert>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-card variant="outlined">
|
||||
<v-card-title>How to Use</v-card-title>
|
||||
<v-card-text>
|
||||
<p class="mb-3">Click the chevron icon in the sidebar header to collapse/expand it.</p>
|
||||
<p class="mb-3">On mobile devices, use the hamburger menu to toggle the sidebar.</p>
|
||||
<p>The sidebar automatically adjusts based on your role permissions.</p>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-card variant="outlined">
|
||||
<v-card-title>Navigation Items</v-card-title>
|
||||
<v-card-text>
|
||||
<p class="mb-3">The sidebar shows different menu items based on your role:</p>
|
||||
<ul class="pl-4">
|
||||
<li><strong>All users:</strong> Dashboard, Analytics, Berth List, Interest List, File Browser</li>
|
||||
<li><strong>Sales/Admin:</strong> + Expenses, Interest Emails</li>
|
||||
<li><strong>Admin only:</strong> + Admin Console</li>
|
||||
</ul>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-alert type="info" variant="tonal" class="mt-6">
|
||||
<div class="text-subtitle-1 font-weight-medium mb-2">Technical Details</div>
|
||||
<p class="text-body-2">This sidebar uses Nuxt UI's <code>UDashboardSidebar</code> component with custom styling to match your brand colors and maintain a clean, professional look.</p>
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
middleware: ['authentication'],
|
||||
layout: 'dashboard-unified'
|
||||
});
|
||||
|
||||
useHead({
|
||||
title: 'Sidebar Demo'
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user