299 lines
8.6 KiB
Vue
299 lines
8.6 KiB
Vue
|
|
<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>
|