Private
Public Access
1
0

Implement Phase 5: reconciliation, SignalR notifications, payees, splits & plugins

Add full frontend UI for five backend features: bank reconciliation (3-step
stepper workflow), payee management (CRUD with alias chips), transaction splits
(expandable rows + split editor), plugins (card grid), and real-time SignalR
notifications (bell dropdown with badge). Fix backend PayeeResponse to expose
alias IDs for deletion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Catherine Renelle
2026-02-08 14:14:23 -05:00
parent d0c3e3913c
commit eb49147080
14 changed files with 1084 additions and 18 deletions
+42 -1
View File
@@ -52,7 +52,37 @@
:icon="isDark ? 'mdi-weather-sunny' : 'mdi-weather-night'"
@click="toggleTheme"
/>
<v-btn icon="mdi-bell-outline" />
<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)"
>
<v-list-item-title>{{ n.data.fileName }}</v-list-item-title>
<v-list-item-subtitle>
{{ n.data.newTransactions }} new, {{ n.data.possibleDuplicates }} duplicates
</v-list-item-subtitle>
</v-list-item>
</template>
</v-list>
</v-menu>
<v-menu>
<template v-slot:activator="{ props }">
<v-btn icon v-bind="props">
@@ -82,6 +112,8 @@ 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 type { AppNotification } from '@/types'
const drawer = ref(true)
const rail = ref(false)
@@ -90,6 +122,8 @@ 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 = [
@@ -97,8 +131,10 @@ const navItems = [
{ 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' },
@@ -119,6 +155,11 @@ function toggleTheme() {
theme.global.name.value = isDark.value ? 'light' : 'dark'
}
function handleNotificationClick(n: AppNotification) {
markAsRead(n.id)
router.push('/imports')
}
function handleLogout() {
authStore.logout()
router.push('/login')