2025-07-11 22:33:11 +02:00
|
|
|
<template>
|
2025-07-11 23:04:13 +02:00
|
|
|
<v-app>
|
|
|
|
|
<v-navigation-drawer
|
|
|
|
|
v-model="drawer"
|
|
|
|
|
:rail="rail"
|
|
|
|
|
permanent
|
|
|
|
|
color="white"
|
|
|
|
|
class="elevation-2"
|
2025-07-11 22:44:29 +02:00
|
|
|
>
|
2025-07-11 23:04:13 +02:00
|
|
|
<!-- Logo and Title -->
|
|
|
|
|
<v-list>
|
|
|
|
|
<v-list-item
|
2025-07-11 23:30:44 +02:00
|
|
|
class="px-3 py-3 cursor-pointer"
|
2025-07-11 23:20:34 +02:00
|
|
|
@click="rail = !rail"
|
2025-07-11 23:04:13 +02:00
|
|
|
>
|
2025-07-11 23:30:44 +02:00
|
|
|
<template v-slot:prepend>
|
|
|
|
|
<v-avatar size="40" class="me-3">
|
|
|
|
|
<v-img src="/Port Nimara New Logo-Circular Frame.png" alt="Port Nimara" />
|
|
|
|
|
</v-avatar>
|
|
|
|
|
</template>
|
|
|
|
|
<v-list-item-title v-if="!rail" class="text-subtitle-1 font-weight-medium">
|
|
|
|
|
Port Nimara CRM
|
|
|
|
|
</v-list-item-title>
|
2025-07-11 23:04:13 +02:00
|
|
|
</v-list-item>
|
|
|
|
|
</v-list>
|
2025-07-11 22:33:11 +02:00
|
|
|
|
2025-07-11 23:04:13 +02:00
|
|
|
<v-divider></v-divider>
|
2025-07-11 22:33:11 +02:00
|
|
|
|
2025-07-11 23:04:13 +02:00
|
|
|
<!-- Navigation Items -->
|
|
|
|
|
<v-list density="compact" nav>
|
|
|
|
|
<v-list-item
|
|
|
|
|
v-for="item in navigationItems"
|
|
|
|
|
:key="item.to"
|
|
|
|
|
:prepend-icon="item.icon"
|
|
|
|
|
:title="item.label"
|
|
|
|
|
:value="item.to"
|
|
|
|
|
:to="item.to"
|
|
|
|
|
color="primary"
|
|
|
|
|
rounded="xl"
|
|
|
|
|
class="mx-1"
|
|
|
|
|
></v-list-item>
|
|
|
|
|
</v-list>
|
2025-07-11 22:33:11 +02:00
|
|
|
|
2025-07-11 23:04:13 +02:00
|
|
|
<template v-slot:append>
|
|
|
|
|
<v-divider></v-divider>
|
|
|
|
|
|
|
|
|
|
<!-- User Info Section -->
|
|
|
|
|
<div class="pa-2">
|
|
|
|
|
<v-list v-if="authState?.user">
|
|
|
|
|
<v-list-item
|
|
|
|
|
:prepend-avatar="`https://ui-avatars.com/api/?name=${encodeURIComponent(authState.user.name || authState.user.email)}&background=387bca&color=fff`"
|
|
|
|
|
:title="authState.user.name || authState.user.email"
|
|
|
|
|
:subtitle="authState.user.email"
|
|
|
|
|
class="px-2"
|
2025-07-11 22:44:29 +02:00
|
|
|
>
|
2025-07-11 23:04:13 +02:00
|
|
|
<template v-slot:append v-if="!rail && authState?.groups?.length">
|
|
|
|
|
<div>
|
|
|
|
|
<v-chip
|
|
|
|
|
v-if="authState.groups.includes('admin')"
|
|
|
|
|
size="small"
|
|
|
|
|
color="orange"
|
|
|
|
|
variant="tonal"
|
|
|
|
|
>
|
|
|
|
|
Admin
|
|
|
|
|
</v-chip>
|
|
|
|
|
<v-chip
|
|
|
|
|
v-else-if="authState.groups.includes('sales')"
|
|
|
|
|
size="small"
|
|
|
|
|
color="green"
|
|
|
|
|
variant="tonal"
|
|
|
|
|
>
|
|
|
|
|
Sales
|
|
|
|
|
</v-chip>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</v-list-item>
|
2025-07-11 22:44:29 +02:00
|
|
|
|
2025-07-11 23:04:13 +02:00
|
|
|
<v-list-item
|
|
|
|
|
@click="handleLogout"
|
|
|
|
|
prepend-icon="mdi-logout"
|
|
|
|
|
title="Logout"
|
|
|
|
|
class="px-2 mt-1"
|
|
|
|
|
base-color="error"
|
|
|
|
|
rounded="xl"
|
|
|
|
|
></v-list-item>
|
|
|
|
|
</v-list>
|
2025-07-11 22:33:11 +02:00
|
|
|
</div>
|
2025-07-11 23:04:13 +02:00
|
|
|
</template>
|
|
|
|
|
</v-navigation-drawer>
|
|
|
|
|
|
|
|
|
|
<v-app-bar
|
|
|
|
|
flat
|
|
|
|
|
color="white"
|
|
|
|
|
class="border-b"
|
|
|
|
|
>
|
|
|
|
|
<v-app-bar-nav-icon
|
|
|
|
|
@click="drawer = !drawer"
|
|
|
|
|
class="d-lg-none"
|
|
|
|
|
></v-app-bar-nav-icon>
|
|
|
|
|
|
|
|
|
|
<v-toolbar-title class="text-h6">
|
|
|
|
|
{{ pageTitle }}
|
|
|
|
|
</v-toolbar-title>
|
|
|
|
|
|
|
|
|
|
<v-spacer></v-spacer>
|
|
|
|
|
</v-app-bar>
|
2025-07-11 22:44:29 +02:00
|
|
|
|
2025-07-11 23:04:13 +02:00
|
|
|
<v-main class="bg-grey-lighten-4">
|
|
|
|
|
<slot />
|
|
|
|
|
</v-main>
|
|
|
|
|
</v-app>
|
2025-07-11 22:33:11 +02:00
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2025-07-11 23:04:13 +02:00
|
|
|
import { ref, computed, onMounted } from 'vue';
|
|
|
|
|
import { useDisplay } from 'vuetify';
|
2025-07-11 22:33:11 +02:00
|
|
|
|
|
|
|
|
const route = useRoute();
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
const nuxtApp = useNuxtApp();
|
2025-07-11 23:04:13 +02:00
|
|
|
const { mdAndDown } = useDisplay();
|
2025-07-11 22:33:11 +02:00
|
|
|
|
|
|
|
|
// Sidebar state
|
2025-07-11 23:04:13 +02:00
|
|
|
const drawer = ref(true);
|
|
|
|
|
const rail = ref(false);
|
2025-07-11 22:33:11 +02:00
|
|
|
|
|
|
|
|
// 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',
|
2025-07-11 23:04:13 +02:00
|
|
|
'dashboard-index': 'Dashboard',
|
2025-07-11 22:33:11 +02:00
|
|
|
'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',
|
2025-07-11 23:04:13 +02:00
|
|
|
'dashboard-admin-index': 'Admin Console',
|
2025-07-11 22:33:11 +02:00
|
|
|
'dashboard-admin-audit-logs': 'Audit Logs',
|
|
|
|
|
'dashboard-admin-system-logs': 'System Logs',
|
|
|
|
|
'dashboard-admin-duplicates': 'Duplicate Management',
|
2025-07-11 23:04:13 +02:00
|
|
|
'dashboard-sidebar-demo': 'Sidebar Demo',
|
2025-07-11 22:33:11 +02:00
|
|
|
};
|
|
|
|
|
|
2025-07-11 23:04:13 +02:00
|
|
|
return pageTitles[routeName] || 'Dashboard';
|
2025-07-11 22:33:11 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Navigation items based on user role
|
2025-07-11 23:04:13 +02:00
|
|
|
const navigationItems = computed(() => {
|
|
|
|
|
const items = [
|
2025-07-11 22:33:11 +02:00
|
|
|
{
|
2025-07-11 23:30:44 +02:00
|
|
|
label: 'Interest List',
|
|
|
|
|
icon: 'mdi-account-multiple',
|
|
|
|
|
to: '/dashboard/interest-list',
|
2025-07-11 22:33:11 +02:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'Analytics',
|
2025-07-11 23:04:13 +02:00
|
|
|
icon: 'mdi-chart-bar',
|
2025-07-11 22:33:11 +02:00
|
|
|
to: '/dashboard/interest-analytics',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'Berth List',
|
2025-07-11 23:04:13 +02:00
|
|
|
icon: 'mdi-table',
|
2025-07-11 22:33:11 +02:00
|
|
|
to: '/dashboard/interest-berth-list',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'Berth Status',
|
2025-07-11 23:04:13 +02:00
|
|
|
icon: 'mdi-map',
|
2025-07-11 22:33:11 +02:00
|
|
|
to: '/dashboard/interest-berth-status',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'Interest Status',
|
2025-07-11 23:04:13 +02:00
|
|
|
icon: 'mdi-clipboard-check',
|
2025-07-11 22:33:11 +02:00
|
|
|
to: '/dashboard/interest-status',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'File Browser',
|
2025-07-11 23:04:13 +02:00
|
|
|
icon: 'mdi-folder-open',
|
2025-07-11 22:33:11 +02:00
|
|
|
to: '/dashboard/file-browser',
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Add sales/admin specific items
|
|
|
|
|
if (authState.value?.groups?.includes('sales') || authState.value?.groups?.includes('admin')) {
|
|
|
|
|
items.push(
|
|
|
|
|
{
|
|
|
|
|
label: 'Expenses',
|
2025-07-11 23:04:13 +02:00
|
|
|
icon: 'mdi-receipt',
|
2025-07-11 22:33:11 +02:00
|
|
|
to: '/dashboard/expenses',
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add admin-only items
|
|
|
|
|
if (authState.value?.groups?.includes('admin')) {
|
|
|
|
|
items.push({
|
|
|
|
|
label: 'Admin Console',
|
2025-07-11 23:04:13 +02:00
|
|
|
icon: 'mdi-shield-crown',
|
2025-07-11 22:33:11 +02:00
|
|
|
to: '/dashboard/admin',
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-11 22:44:29 +02:00
|
|
|
return items;
|
2025-07-11 22:33:11 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 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');
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-07-11 23:04:13 +02:00
|
|
|
|
|
|
|
|
// Initialize drawer state on mobile
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
if (mdAndDown.value) {
|
|
|
|
|
drawer.value = false;
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-07-11 22:33:11 +02:00
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2025-07-11 23:04:13 +02:00
|
|
|
.border-b {
|
|
|
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.12) !important;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-11 23:20:34 +02:00
|
|
|
.cursor-pointer {
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Improve rail mode appearance */
|
|
|
|
|
.v-navigation-drawer--rail {
|
|
|
|
|
width: 72px !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.v-navigation-drawer--rail .v-list-item {
|
|
|
|
|
padding-inline-start: 12px !important;
|
|
|
|
|
padding-inline-end: 12px !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.v-navigation-drawer--rail .v-list-item__prepend {
|
|
|
|
|
margin-inline-end: 0 !important;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-11 23:04:13 +02:00
|
|
|
.v-navigation-drawer--rail .v-list-item__append {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.v-navigation-drawer--rail.v-navigation-drawer--is-hovering .v-list-item__append {
|
|
|
|
|
display: flex;
|
2025-07-11 22:33:11 +02:00
|
|
|
}
|
2025-07-11 23:20:34 +02:00
|
|
|
|
|
|
|
|
/* Ensure proper mobile responsiveness */
|
|
|
|
|
@media (max-width: 960px) {
|
|
|
|
|
.v-navigation-drawer {
|
|
|
|
|
position: fixed !important;
|
|
|
|
|
z-index: 1004 !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.v-main {
|
|
|
|
|
padding-left: 0 !important;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* PWA optimizations */
|
|
|
|
|
@media (display-mode: standalone) {
|
|
|
|
|
.v-app-bar {
|
|
|
|
|
padding-top: env(safe-area-inset-top);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.v-navigation-drawer {
|
|
|
|
|
padding-bottom: env(safe-area-inset-bottom);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Improve visual alignment */
|
|
|
|
|
.v-list-item__prepend > .v-avatar {
|
|
|
|
|
margin-inline-end: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.v-navigation-drawer--rail .v-list-item__prepend > .v-avatar {
|
|
|
|
|
margin-inline-end: 0;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-11 23:30:44 +02:00
|
|
|
/* Center logo in rail mode */
|
|
|
|
|
.v-navigation-drawer--rail .v-list-item {
|
|
|
|
|
justify-content: center;
|
|
|
|
|
padding-inline-start: 16px !important;
|
|
|
|
|
padding-inline-end: 16px !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.v-navigation-drawer--rail .v-list-item__prepend {
|
|
|
|
|
justify-content: center;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-11 23:20:34 +02:00
|
|
|
/* Smooth transitions */
|
|
|
|
|
.v-navigation-drawer,
|
|
|
|
|
.v-list-item__content,
|
|
|
|
|
.v-list-item__prepend {
|
|
|
|
|
transition: all 0.2s ease-in-out;
|
|
|
|
|
}
|
2025-07-11 22:33:11 +02:00
|
|
|
</style>
|