f148b35ab2
- Show estimated payoff amount (balance + accrued interest) in loan summary - Display loan balances as absolute values in red - Fix dialog overlay blocking UI after edit by closing in finally with nextTick - Highlight parent nav item when viewing sub-routes (e.g. /loans/:id) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
207 lines
6.8 KiB
Vue
207 lines
6.8 KiB
Vue
<template>
|
|
<v-navigation-drawer v-model="drawer" :rail="rail" permanent>
|
|
<v-list-item
|
|
prepend-icon="mdi-cat"
|
|
title="Purrse"
|
|
subtitle="Personal Finance"
|
|
nav
|
|
@click="rail = !rail"
|
|
class="drawer-header"
|
|
>
|
|
<template v-slot:append>
|
|
<v-icon>mdi-chevron-left</v-icon>
|
|
</template>
|
|
</v-list-item>
|
|
|
|
<v-divider />
|
|
|
|
<v-list density="compact" nav>
|
|
<v-list-item
|
|
v-for="item in navItems"
|
|
:key="item.route"
|
|
:prepend-icon="item.icon"
|
|
:title="item.title"
|
|
:to="item.route"
|
|
:value="item.route"
|
|
:active="isNavActive(item.route)"
|
|
rounded="xl"
|
|
>
|
|
<v-tooltip v-if="rail" activator="parent" location="end">{{ item.title }}</v-tooltip>
|
|
</v-list-item>
|
|
</v-list>
|
|
|
|
<template v-slot:append>
|
|
<v-divider />
|
|
<v-list density="compact" nav>
|
|
<v-list-item
|
|
prepend-icon="mdi-logout"
|
|
title="Logout"
|
|
@click="handleLogout"
|
|
rounded="xl"
|
|
>
|
|
<v-tooltip v-if="rail" activator="parent" location="end">Logout</v-tooltip>
|
|
</v-list-item>
|
|
</v-list>
|
|
</template>
|
|
</v-navigation-drawer>
|
|
|
|
<v-app-bar density="compact" flat>
|
|
<v-app-bar-title>
|
|
<span class="text-body-1 font-weight-medium">{{ currentPageTitle }}</span>
|
|
</v-app-bar-title>
|
|
|
|
<template v-slot:append>
|
|
<v-btn
|
|
:icon="isDark ? 'mdi-weather-sunny' : 'mdi-weather-night'"
|
|
@click="toggleTheme"
|
|
/>
|
|
<v-menu>
|
|
<template v-slot:activator="{ props: bellProps }">
|
|
<v-btn icon v-bind="bellProps">
|
|
<v-badge :content="unreadCount" :model-value="unreadCount > 0" color="error" floating>
|
|
<v-icon>mdi-bell-outline</v-icon>
|
|
</v-badge>
|
|
</v-btn>
|
|
</template>
|
|
<v-list density="compact" min-width="320" max-height="400">
|
|
<v-list-item v-if="notifications.length === 0">
|
|
<v-list-item-title class="text-medium-emphasis text-center">No notifications</v-list-item-title>
|
|
</v-list-item>
|
|
<template v-else>
|
|
<v-list-item class="d-flex justify-end">
|
|
<v-btn variant="text" size="small" @click="markAllAsRead">Mark all as read</v-btn>
|
|
</v-list-item>
|
|
<v-divider />
|
|
<v-list-item
|
|
v-for="n in notifications"
|
|
:key="n.id"
|
|
:class="{ 'bg-blue-lighten-5': !n.read }"
|
|
@click="handleNotificationClick(n)"
|
|
>
|
|
<template v-if="n.type === 'FileImportReady'">
|
|
<v-list-item-title>{{ (n.data as any).fileName }}</v-list-item-title>
|
|
<v-list-item-subtitle>
|
|
{{ (n.data as any).newTransactions }} new, {{ (n.data as any).possibleDuplicates }} duplicates
|
|
</v-list-item-subtitle>
|
|
</template>
|
|
<template v-else-if="n.type === 'BankSyncComplete'">
|
|
<v-list-item-title>{{ (n.data as any).institutionName }}</v-list-item-title>
|
|
<v-list-item-subtitle>
|
|
{{ (n.data as any).transactionsImported }} imported, {{ (n.data as any).transactionsSkipped }} skipped
|
|
</v-list-item-subtitle>
|
|
</template>
|
|
</v-list-item>
|
|
</template>
|
|
</v-list>
|
|
</v-menu>
|
|
<v-menu>
|
|
<template v-slot:activator="{ props }">
|
|
<v-btn icon v-bind="props">
|
|
<v-avatar color="primary" size="32">
|
|
<span class="text-body-2">{{ userInitial }}</span>
|
|
</v-avatar>
|
|
</v-btn>
|
|
</template>
|
|
<v-list density="compact">
|
|
<v-list-item prepend-icon="mdi-account" :title="authStore.username || 'User'" />
|
|
<v-divider />
|
|
<v-list-item prepend-icon="mdi-cog" title="Settings" @click="router.push('/settings')" />
|
|
<v-list-item prepend-icon="mdi-logout" title="Logout" @click="handleLogout" />
|
|
</v-list>
|
|
</v-menu>
|
|
</template>
|
|
</v-app-bar>
|
|
|
|
<v-main>
|
|
<v-container fluid class="pa-6">
|
|
<slot />
|
|
</v-container>
|
|
</v-main>
|
|
|
|
<ChatPanel />
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { useTheme } from 'vuetify'
|
|
import { useAuthStore } from '@/stores/auth'
|
|
import { useNotifications } from '@/composables/useNotifications'
|
|
import ChatPanel from '@/components/chat/ChatPanel.vue'
|
|
import type { AppNotification } from '@/types'
|
|
|
|
const drawer = ref(true)
|
|
const rail = ref(false)
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const theme = useTheme()
|
|
const authStore = useAuthStore()
|
|
|
|
const { notifications, unreadCount, markAllAsRead, markAsRead } = useNotifications()
|
|
|
|
const isDark = computed(() => theme.global.current.value.dark)
|
|
|
|
const navItems = [
|
|
{ title: 'Dashboard', icon: 'mdi-view-dashboard', route: '/' },
|
|
{ title: 'Accounts', icon: 'mdi-bank', route: '/accounts' },
|
|
{ title: 'Transactions', icon: 'mdi-swap-horizontal', route: '/transactions' },
|
|
{ title: 'Categories', icon: 'mdi-tag-multiple', route: '/categories' },
|
|
{ title: 'Payees', icon: 'mdi-account-group', route: '/payees' },
|
|
{ title: 'Budgets', icon: 'mdi-calculator', route: '/budgets' },
|
|
{ title: 'Scheduled', icon: 'mdi-clock-outline', route: '/scheduled' },
|
|
{ title: 'Reconciliation', icon: 'mdi-scale-balance', route: '/reconciliation' },
|
|
{ title: 'Imports', icon: 'mdi-file-upload', route: '/imports' },
|
|
{ title: 'Loans', icon: 'mdi-home-city', route: '/loans' },
|
|
{ title: 'Reports', icon: 'mdi-chart-bar', route: '/reports' },
|
|
{ title: 'Investments', icon: 'mdi-finance', route: '/investments' },
|
|
{ title: 'Settings', icon: 'mdi-cog', route: '/settings' },
|
|
]
|
|
|
|
function isNavActive(navRoute: string) {
|
|
if (route.path === navRoute) return true
|
|
// Highlight parent nav for sub-routes (e.g. /loans/:id highlights Loans)
|
|
if (navRoute !== '/' && route.path.startsWith(navRoute + '/')) return true
|
|
// Highlight "Transactions" when viewing account-scoped transactions
|
|
if (navRoute === '/transactions' && route.name === 'account-transactions') return true
|
|
return false
|
|
}
|
|
|
|
const currentPageTitle = computed(() => {
|
|
const item = navItems.find(n => isNavActive(n.route))
|
|
return item?.title || 'Purrse'
|
|
})
|
|
|
|
const userInitial = computed(() => {
|
|
return authStore.username?.charAt(0).toUpperCase() || 'U'
|
|
})
|
|
|
|
function toggleTheme() {
|
|
theme.global.name.value = isDark.value ? 'light' : 'dark'
|
|
}
|
|
|
|
function handleNotificationClick(n: AppNotification) {
|
|
markAsRead(n.id)
|
|
if (n.type === 'BankSyncComplete') {
|
|
router.push('/settings')
|
|
} else {
|
|
router.push('/imports')
|
|
}
|
|
}
|
|
|
|
function handleLogout() {
|
|
authStore.logout()
|
|
router.push('/login')
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.drawer-header {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.v-navigation-drawer--rail .drawer-header :deep(.v-list-item__prepend) {
|
|
margin-inline-end: 0;
|
|
justify-content: center;
|
|
}
|
|
</style>
|