269 lines
8.0 KiB
Vue
269 lines
8.0 KiB
Vue
<template>
|
|
<div class="min-h-screen bg-gray-50 flex">
|
|
<!-- Sidebar -->
|
|
<div
|
|
:class="[
|
|
'bg-white border-r border-gray-200 transition-all duration-300',
|
|
sidebarOpen ? 'w-64' : 'w-0 overflow-hidden',
|
|
'lg:w-64'
|
|
]"
|
|
>
|
|
<div class="h-full flex flex-col">
|
|
<!-- Header -->
|
|
<div class="px-4 py-4 border-b border-gray-100">
|
|
<div class="flex items-center gap-3">
|
|
<img
|
|
src="/Port Nimara New Logo-Circular Frame.png"
|
|
class="h-8 w-8"
|
|
alt="Port Nimara"
|
|
>
|
|
<div 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>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Navigation -->
|
|
<nav class="flex-1 px-3 py-4 space-y-1 overflow-y-auto">
|
|
<NuxtLink
|
|
v-for="item in navigationItems"
|
|
:key="item.to"
|
|
:to="item.to"
|
|
class="group flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors duration-200"
|
|
:class="[
|
|
route.path === item.to
|
|
? 'bg-blue-50 text-blue-700'
|
|
: 'text-gray-700 hover:bg-gray-100'
|
|
]"
|
|
>
|
|
<Icon
|
|
:name="item.icon"
|
|
class="w-5 h-5 flex-shrink-0"
|
|
:class="[
|
|
route.path === item.to
|
|
? 'text-blue-700'
|
|
: 'text-gray-400 group-hover:text-gray-600'
|
|
]"
|
|
/>
|
|
<span class="truncate">{{ item.label }}</span>
|
|
</NuxtLink>
|
|
</nav>
|
|
|
|
<!-- Footer -->
|
|
<div class="px-4 py-4 border-t border-gray-100 space-y-2">
|
|
<!-- User Info -->
|
|
<div v-if="authState?.user" class="flex items-center gap-3 p-2">
|
|
<div class="flex-shrink-0">
|
|
<div class="w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center text-white text-sm font-medium">
|
|
{{ (authState.user.name || authState.user.email || '?')[0].toUpperCase() }}
|
|
</div>
|
|
</div>
|
|
<div 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="authState?.groups?.length" class="px-2">
|
|
<div
|
|
v-if="authState.groups.includes('admin')"
|
|
class="inline-flex items-center justify-center w-full px-2 py-1 text-xs font-medium text-orange-700 bg-orange-100 rounded-md"
|
|
>
|
|
Admin
|
|
</div>
|
|
<div
|
|
v-else-if="authState.groups.includes('sales')"
|
|
class="inline-flex items-center justify-center w-full px-2 py-1 text-xs font-medium text-green-700 bg-green-100 rounded-md"
|
|
>
|
|
Sales
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Logout Button -->
|
|
<button
|
|
@click="handleLogout"
|
|
class="w-full flex items-center justify-start gap-3 px-3 py-2 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-100 transition-colors duration-200"
|
|
>
|
|
<Icon name="i-heroicons-arrow-left-on-rectangle" class="w-5 h-5 text-gray-400" />
|
|
<span>Logout</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main Content -->
|
|
<div class="flex-1 flex flex-col">
|
|
<!-- Top Bar -->
|
|
<header class="bg-white border-b border-gray-200 px-4 py-3">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-4">
|
|
<!-- Mobile Menu Toggle -->
|
|
<button
|
|
@click="sidebarOpen = !sidebarOpen"
|
|
class="lg:hidden p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100"
|
|
>
|
|
<Icon name="i-heroicons-bars-3" class="w-6 h-6" />
|
|
</button>
|
|
|
|
<!-- Page Title -->
|
|
<h1 class="text-xl font-semibold text-gray-900">{{ pageTitle }}</h1>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Page Content -->
|
|
<main class="flex-1 p-6 overflow-y-auto">
|
|
<slot />
|
|
</main>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed } from 'vue';
|
|
|
|
// Define NavigationMenuItem type locally
|
|
interface NavigationMenuItem {
|
|
label: string;
|
|
icon: string;
|
|
to: string;
|
|
}
|
|
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
const nuxtApp = useNuxtApp();
|
|
|
|
// Sidebar state
|
|
const sidebarOpen = 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;
|
|
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 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>
|
|
/* Ensure proper responsive behavior */
|
|
@media (min-width: 1024px) {
|
|
.lg\:w-64 {
|
|
width: 16rem !important;
|
|
}
|
|
}
|
|
</style>
|