Private
Public Access
1
0
Files
Purrse/frontend/src/components/layout/AppLayout.vue
T
Catherine Renelle f148b35ab2 Add payoff amount, fix dialog overlay and nav highlight for sub-routes
- 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>
2026-02-14 22:12:50 -05:00

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>