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:
parent
d71e2d348c
commit
61235b163d
|
|
@ -0,0 +1,298 @@
|
||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-gray-50">
|
||||||
|
<!-- Main Dashboard Group -->
|
||||||
|
<UDashboardGroup>
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<UDashboardSidebar
|
||||||
|
v-model:open="sidebarOpen"
|
||||||
|
:collapsible="true"
|
||||||
|
:resizable="false"
|
||||||
|
:ui="{
|
||||||
|
wrapper: 'bg-white border-r border-gray-200',
|
||||||
|
header: 'px-4 py-4 border-b border-gray-100',
|
||||||
|
body: 'px-3 py-4',
|
||||||
|
footer: 'px-4 py-4 border-t border-gray-100'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #header="{ collapsed }">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<img
|
||||||
|
src="/Port Nimara New Logo-Circular Frame.png"
|
||||||
|
class="h-8 w-8"
|
||||||
|
alt="Port Nimara"
|
||||||
|
>
|
||||||
|
<transition name="fade">
|
||||||
|
<div v-if="!collapsed" class="flex flex-col">
|
||||||
|
<span class="text-sm font-semibold text-gray-900">Port Nimara</span>
|
||||||
|
<span class="text-xs text-gray-500">Client Portal</span>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #default="{ collapsed }">
|
||||||
|
<!-- Navigation Menu -->
|
||||||
|
<UNavigationMenu
|
||||||
|
:collapsed="collapsed"
|
||||||
|
:items="navigationItems"
|
||||||
|
orientation="vertical"
|
||||||
|
:ui="{
|
||||||
|
wrapper: 'space-y-1',
|
||||||
|
base: 'group flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors duration-200',
|
||||||
|
active: 'bg-blue-50 text-blue-700',
|
||||||
|
inactive: 'text-gray-700 hover:bg-gray-100',
|
||||||
|
icon: {
|
||||||
|
base: 'w-5 h-5 flex-shrink-0',
|
||||||
|
active: 'text-blue-700',
|
||||||
|
inactive: 'text-gray-400 group-hover:text-gray-600'
|
||||||
|
},
|
||||||
|
label: 'truncate',
|
||||||
|
badge: 'ml-auto'
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #footer="{ collapsed }">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<!-- User Info -->
|
||||||
|
<div v-if="authState?.user" class="flex items-center gap-3 p-2">
|
||||||
|
<UAvatar
|
||||||
|
:src="`https://ui-avatars.com/api/?name=${encodeURIComponent(authState.user.name || authState.user.email)}&background=387bca&color=fff`"
|
||||||
|
:alt="authState.user.name || authState.user.email"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<div v-if="!collapsed" class="flex-1 min-w-0">
|
||||||
|
<p class="text-sm font-medium text-gray-900 truncate">
|
||||||
|
{{ authState.user.name || authState.user.email }}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-gray-500 truncate">
|
||||||
|
{{ authState.user.email }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Role Badge -->
|
||||||
|
<div v-if="!collapsed && authState?.groups?.length" class="px-2">
|
||||||
|
<UBadge
|
||||||
|
v-if="authState.groups.includes('admin')"
|
||||||
|
color="orange"
|
||||||
|
variant="subtle"
|
||||||
|
size="sm"
|
||||||
|
class="w-full justify-center"
|
||||||
|
>
|
||||||
|
Admin
|
||||||
|
</UBadge>
|
||||||
|
<UBadge
|
||||||
|
v-else-if="authState.groups.includes('sales')"
|
||||||
|
color="green"
|
||||||
|
variant="subtle"
|
||||||
|
size="sm"
|
||||||
|
class="w-full justify-center"
|
||||||
|
>
|
||||||
|
Sales
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Logout Button -->
|
||||||
|
<UButton
|
||||||
|
@click="handleLogout"
|
||||||
|
:icon="collapsed ? 'i-heroicons-arrow-left-on-rectangle' : undefined"
|
||||||
|
:label="collapsed ? undefined : 'Logout'"
|
||||||
|
color="gray"
|
||||||
|
variant="ghost"
|
||||||
|
:block="!collapsed"
|
||||||
|
:square="collapsed"
|
||||||
|
class="w-full justify-start"
|
||||||
|
>
|
||||||
|
<template v-if="!collapsed" #leading>
|
||||||
|
<UIcon name="i-heroicons-arrow-left-on-rectangle" class="w-5 h-5" />
|
||||||
|
</template>
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UDashboardSidebar>
|
||||||
|
|
||||||
|
<!-- Main Panel -->
|
||||||
|
<UDashboardPanel>
|
||||||
|
<template #header>
|
||||||
|
<UDashboardNavbar :title="pageTitle" class="bg-white border-b border-gray-200">
|
||||||
|
<template #left>
|
||||||
|
<UDashboardSidebarToggle
|
||||||
|
v-if="mdAndDown"
|
||||||
|
variant="ghost"
|
||||||
|
color="gray"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #right>
|
||||||
|
<!-- Additional navbar content can go here -->
|
||||||
|
</template>
|
||||||
|
</UDashboardNavbar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Page Content -->
|
||||||
|
<div class="p-6">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</UDashboardPanel>
|
||||||
|
</UDashboardGroup>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
|
||||||
|
// Define NavigationMenuItem type locally
|
||||||
|
interface NavigationMenuItem {
|
||||||
|
label: string;
|
||||||
|
icon?: string;
|
||||||
|
to?: string;
|
||||||
|
badge?: string | number;
|
||||||
|
click?: () => void;
|
||||||
|
defaultOpen?: boolean;
|
||||||
|
children?: NavigationMenuItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const nuxtApp = useNuxtApp();
|
||||||
|
const { mdAndDown } = useDisplay();
|
||||||
|
|
||||||
|
// Sidebar state
|
||||||
|
const sidebarOpen = ref(true);
|
||||||
|
|
||||||
|
// Get auth state - with fallback to prevent errors
|
||||||
|
const authState = computed(() => {
|
||||||
|
const data = nuxtApp.payload?.data?.authState;
|
||||||
|
// Only return data if it's properly initialized
|
||||||
|
if (data && data.authenticated !== undefined) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Page title based on current route
|
||||||
|
const pageTitle = computed(() => {
|
||||||
|
const routeName = route.name as string;
|
||||||
|
const pageTitles: Record<string, string> = {
|
||||||
|
'dashboard': 'Dashboard',
|
||||||
|
'dashboard-expenses': 'Expense Tracking',
|
||||||
|
'dashboard-interest-list': 'Interest List',
|
||||||
|
'dashboard-berth-list': 'Berth List',
|
||||||
|
'dashboard-interest-status': 'Interest Status',
|
||||||
|
'dashboard-interest-emails': 'Interest Emails',
|
||||||
|
'dashboard-interest-berth-list': 'Interest Berth List',
|
||||||
|
'dashboard-interest-berth-status': 'Berth Status',
|
||||||
|
'dashboard-interest-analytics': 'Analytics',
|
||||||
|
'dashboard-file-browser': 'File Browser',
|
||||||
|
'dashboard-admin': 'Admin Console',
|
||||||
|
'dashboard-admin-audit-logs': 'Audit Logs',
|
||||||
|
'dashboard-admin-system-logs': 'System Logs',
|
||||||
|
'dashboard-admin-duplicates': 'Duplicate Management',
|
||||||
|
};
|
||||||
|
|
||||||
|
return pageTitles[routeName] || routeName?.split('-').map(word =>
|
||||||
|
word.charAt(0).toUpperCase() + word.slice(1)
|
||||||
|
).join(' ') || 'Dashboard';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Navigation items based on user role
|
||||||
|
const navigationItems = computed((): NavigationMenuItem[][] => {
|
||||||
|
const items: NavigationMenuItem[] = [
|
||||||
|
{
|
||||||
|
label: 'Dashboard',
|
||||||
|
icon: 'i-heroicons-home',
|
||||||
|
to: '/dashboard',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Analytics',
|
||||||
|
icon: 'i-heroicons-chart-bar',
|
||||||
|
to: '/dashboard/interest-analytics',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Berth List',
|
||||||
|
icon: 'i-heroicons-table-cells',
|
||||||
|
to: '/dashboard/interest-berth-list',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Berth Status',
|
||||||
|
icon: 'i-heroicons-map',
|
||||||
|
to: '/dashboard/interest-berth-status',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Interest List',
|
||||||
|
icon: 'i-heroicons-users',
|
||||||
|
to: '/dashboard/interest-list',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Interest Status',
|
||||||
|
icon: 'i-heroicons-clipboard-document-check',
|
||||||
|
to: '/dashboard/interest-status',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'File Browser',
|
||||||
|
icon: 'i-heroicons-folder-open',
|
||||||
|
to: '/dashboard/file-browser',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add sales/admin specific items
|
||||||
|
if (authState.value?.groups?.includes('sales') || authState.value?.groups?.includes('admin')) {
|
||||||
|
items.push(
|
||||||
|
{
|
||||||
|
label: 'Expenses',
|
||||||
|
icon: 'i-heroicons-receipt-percent',
|
||||||
|
to: '/dashboard/expenses',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Interest Emails',
|
||||||
|
icon: 'i-heroicons-envelope',
|
||||||
|
to: '/dashboard/interest-emails',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add admin-only items
|
||||||
|
if (authState.value?.groups?.includes('admin')) {
|
||||||
|
items.push({
|
||||||
|
label: 'Admin Console',
|
||||||
|
icon: 'i-heroicons-shield-check',
|
||||||
|
to: '/dashboard/admin',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return as nested array for proper spacing
|
||||||
|
return [items];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Logout handler
|
||||||
|
const handleLogout = async () => {
|
||||||
|
try {
|
||||||
|
await $fetch('/api/auth/logout', { method: 'POST' });
|
||||||
|
await router.push('/login');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Logout error:', error);
|
||||||
|
// Even if logout fails, redirect to login
|
||||||
|
await router.push('/login');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize sidebar state based on screen size
|
||||||
|
onMounted(() => {
|
||||||
|
if (mdAndDown.value) {
|
||||||
|
sidebarOpen.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,190 +0,0 @@
|
||||||
<template>
|
|
||||||
<v-app>
|
|
||||||
<!-- Navigation Drawer -->
|
|
||||||
<v-navigation-drawer
|
|
||||||
v-model="drawer"
|
|
||||||
:rail="rail"
|
|
||||||
permanent
|
|
||||||
color="grey-darken-4"
|
|
||||||
>
|
|
||||||
<v-list>
|
|
||||||
<v-list-item
|
|
||||||
prepend-avatar="/Port Nimara New Logo-Circular Frame.png"
|
|
||||||
:title="rail ? '' : 'Port Nimara'"
|
|
||||||
:subtitle="rail ? '' : 'Client Portal'"
|
|
||||||
nav
|
|
||||||
>
|
|
||||||
<template v-slot:append>
|
|
||||||
<v-btn
|
|
||||||
v-if="!rail"
|
|
||||||
icon="mdi-chevron-left"
|
|
||||||
variant="text"
|
|
||||||
@click.stop="rail = !rail"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
|
|
||||||
<v-divider />
|
|
||||||
|
|
||||||
<v-list density="compact" nav>
|
|
||||||
<v-list-item
|
|
||||||
v-for="item in navigationItems"
|
|
||||||
:key="item.title"
|
|
||||||
:to="item.to"
|
|
||||||
:prepend-icon="item.icon"
|
|
||||||
:title="item.title"
|
|
||||||
:value="item.title"
|
|
||||||
color="primary"
|
|
||||||
/>
|
|
||||||
</v-list>
|
|
||||||
|
|
||||||
<template v-slot:append>
|
|
||||||
<v-list density="compact" nav>
|
|
||||||
<v-list-item
|
|
||||||
@click="handleLogout"
|
|
||||||
prepend-icon="mdi-logout"
|
|
||||||
title="Logout"
|
|
||||||
value="logout"
|
|
||||||
color="primary"
|
|
||||||
/>
|
|
||||||
</v-list>
|
|
||||||
</template>
|
|
||||||
</v-navigation-drawer>
|
|
||||||
|
|
||||||
<!-- App Bar -->
|
|
||||||
<v-app-bar
|
|
||||||
color="primary"
|
|
||||||
density="compact"
|
|
||||||
elevation="0"
|
|
||||||
>
|
|
||||||
<v-app-bar-nav-icon
|
|
||||||
@click="rail = !rail"
|
|
||||||
v-if="rail"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<v-toolbar-title>{{ pageTitle }}</v-toolbar-title>
|
|
||||||
|
|
||||||
<v-spacer />
|
|
||||||
|
|
||||||
<v-chip
|
|
||||||
v-if="authState?.user"
|
|
||||||
class="mr-2"
|
|
||||||
color="white"
|
|
||||||
variant="tonal"
|
|
||||||
>
|
|
||||||
<v-icon start>mdi-account</v-icon>
|
|
||||||
{{ authState.user.name || authState.user.email }}
|
|
||||||
</v-chip>
|
|
||||||
|
|
||||||
<v-chip
|
|
||||||
v-if="authState?.groups?.includes('admin')"
|
|
||||||
color="orange"
|
|
||||||
variant="tonal"
|
|
||||||
class="mr-2"
|
|
||||||
>
|
|
||||||
Admin
|
|
||||||
</v-chip>
|
|
||||||
|
|
||||||
<v-chip
|
|
||||||
v-else-if="authState?.groups?.includes('sales')"
|
|
||||||
color="green"
|
|
||||||
variant="tonal"
|
|
||||||
class="mr-2"
|
|
||||||
>
|
|
||||||
Sales
|
|
||||||
</v-chip>
|
|
||||||
</v-app-bar>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
|
||||||
<v-main>
|
|
||||||
<slot />
|
|
||||||
</v-main>
|
|
||||||
</v-app>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, computed } from 'vue';
|
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
const router = useRouter();
|
|
||||||
const nuxtApp = useNuxtApp();
|
|
||||||
|
|
||||||
// Navigation state
|
|
||||||
const drawer = ref(true);
|
|
||||||
const rail = ref(false);
|
|
||||||
|
|
||||||
// Get auth state - with fallback to prevent errors
|
|
||||||
const authState = computed(() => {
|
|
||||||
const data = nuxtApp.payload?.data?.authState;
|
|
||||||
// Only return data if it's properly initialized
|
|
||||||
if (data && data.authenticated !== undefined) {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Page title based on current route
|
|
||||||
const pageTitle = computed(() => {
|
|
||||||
const routeName = route.name as string;
|
|
||||||
if (routeName === 'dashboard') return 'Dashboard';
|
|
||||||
if (routeName === 'dashboard-expenses') return 'Expense Tracking';
|
|
||||||
if (routeName === 'dashboard-interest-list') return 'Interest List';
|
|
||||||
if (routeName === 'dashboard-berth-list') return 'Berth List';
|
|
||||||
if (routeName === 'dashboard-admin-audit-logs') return 'Audit Logs';
|
|
||||||
if (routeName === 'dashboard-admin-system-logs') return 'System Logs';
|
|
||||||
if (routeName === 'dashboard-admin-duplicates') return 'Duplicate Management';
|
|
||||||
|
|
||||||
// Default: capitalize route name
|
|
||||||
return routeName?.split('-').map(word =>
|
|
||||||
word.charAt(0).toUpperCase() + word.slice(1)
|
|
||||||
).join(' ') || 'Dashboard';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Navigation items based on user role
|
|
||||||
const navigationItems = computed(() => {
|
|
||||||
const items = [
|
|
||||||
{ title: 'Dashboard', icon: 'mdi-view-dashboard', to: '/dashboard' },
|
|
||||||
{ title: 'Interest List', icon: 'mdi-account-group', to: '/dashboard/interest-list' },
|
|
||||||
{ title: 'Berth List', icon: 'mdi-sailboat', to: '/dashboard/berth-list' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// Add sales/admin specific items
|
|
||||||
if (authState.value?.groups?.includes('sales') || authState.value?.groups?.includes('admin')) {
|
|
||||||
items.push(
|
|
||||||
{ title: 'Expenses', icon: 'mdi-receipt', to: '/dashboard/expenses' },
|
|
||||||
{ title: 'Interest Status', icon: 'mdi-chart-timeline', to: '/dashboard/interest-status' },
|
|
||||||
{ title: 'Interest Emails', icon: 'mdi-email-multiple', to: '/dashboard/interest-emails' },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add admin-only items
|
|
||||||
if (authState.value?.groups?.includes('admin')) {
|
|
||||||
items.push(
|
|
||||||
{ title: 'Admin', icon: 'mdi-shield-crown', to: '/dashboard/admin' },
|
|
||||||
{ title: 'File Browser', icon: 'mdi-folder-open', to: '/dashboard/file-browser' },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Logout handler
|
|
||||||
const handleLogout = async () => {
|
|
||||||
try {
|
|
||||||
await $fetch('/api/auth/logout', { method: 'POST' });
|
|
||||||
await router.push('/login');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Logout error:', error);
|
|
||||||
// Even if logout fails, redirect to login
|
|
||||||
await router.push('/login');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.v-navigation-drawer {
|
|
||||||
top: 0 !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -2,7 +2,7 @@ export default defineNuxtConfig({
|
||||||
ssr: false,
|
ssr: false,
|
||||||
compatibilityDate: "2024-11-01",
|
compatibilityDate: "2024-11-01",
|
||||||
devtools: { enabled: true },
|
devtools: { enabled: true },
|
||||||
modules: ["nuxt-directus", "vuetify-nuxt-module", "@vite-pwa/nuxt"],
|
modules: ["nuxt-directus", "vuetify-nuxt-module", "@vite-pwa/nuxt", "@nuxt/ui"],
|
||||||
app: {
|
app: {
|
||||||
head: {
|
head: {
|
||||||
titleTemplate: "%s • Port Nimara Portal",
|
titleTemplate: "%s • Port Nimara Portal",
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -8,6 +8,7 @@
|
||||||
"postinstall": "nuxt prepare"
|
"postinstall": "nuxt prepare"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@nuxt/ui": "^3.2.0",
|
||||||
"@pdfme/common": "^5.4.0",
|
"@pdfme/common": "^5.4.0",
|
||||||
"@pdfme/generator": "^5.4.0",
|
"@pdfme/generator": "^5.4.0",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
|
|
|
||||||
|
|
@ -1,293 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<v-app full-height>
|
<div>
|
||||||
<v-navigation-drawer
|
<!-- This page now acts as a parent route for dashboard pages -->
|
||||||
v-model="drawer"
|
<NuxtPage />
|
||||||
:location="mdAndDown ? 'bottom' : undefined"
|
</div>
|
||||||
>
|
|
||||||
<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>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: ["authentication"],
|
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>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -238,6 +238,7 @@ import { formatDate, formatTime, formatDateTime } from '@/utils/dateUtils'
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: ['authentication', 'authorization'],
|
middleware: ['authentication', 'authorization'],
|
||||||
|
layout: 'dashboard-unified',
|
||||||
auth: {
|
auth: {
|
||||||
roles: ['admin']
|
roles: ['admin']
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -230,6 +230,7 @@ import { formatTime, formatDateTime } from '@/utils/dateUtils'
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: ['authentication', 'authorization'],
|
middleware: ['authentication', 'authorization'],
|
||||||
|
layout: 'dashboard-unified',
|
||||||
auth: {
|
auth: {
|
||||||
roles: ['admin']
|
roles: ['admin']
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'dashboard-unified'
|
||||||
|
});
|
||||||
|
|
||||||
const { user, isAuthenticated, authSource, isAdmin, logout } = useUnifiedAuth();
|
const { user, isAuthenticated, authSource, isAdmin, logout } = useUnifiedAuth();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -347,7 +347,7 @@ const ExpenseCreateModal = defineAsyncComponent(() => import('@/components/Expen
|
||||||
// Page meta
|
// Page meta
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: ['authentication', 'authorization'],
|
middleware: ['authentication', 'authorization'],
|
||||||
layout: 'dashboard',
|
layout: 'dashboard-unified',
|
||||||
roles: ['sales', 'admin']
|
roles: ['sales', 'admin']
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -336,6 +336,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'dashboard-unified'
|
||||||
|
});
|
||||||
|
|
||||||
import { ref, computed, onMounted, watch } from 'vue';
|
import { ref, computed, onMounted, watch } from 'vue';
|
||||||
import FileUploader from '~/components/FileUploader.vue';
|
import FileUploader from '~/components/FileUploader.vue';
|
||||||
import FilePreviewModal from '~/components/FilePreviewModal.vue';
|
import FilePreviewModal from '~/components/FilePreviewModal.vue';
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
Loading…
Reference in New Issue