Initial commit: Purrse personal finance app
Self-hosted, plugin-extensible personal finance manager built with ASP.NET Core 9.0, Vue 3 + Vuetify 3, and PostgreSQL 17. Backend (8 .NET projects): - Core: 19 domain models, 6 enums, 14 DTOs, 12 service interfaces - Data: EF Core DbContext, 16 entity configurations, category seeder - API: 14 controllers, 15 services, JWT auth, SignalR, middleware - Plugins: Abstractions + OFX/CSV/QIF file parsers - Tests: 28 xUnit tests (fingerprinting, duplicate detection, parsers) Frontend (Vue 3 + Vuetify 3 + TypeScript): - 13 views, Pinia stores, Axios API services with JWT interceptors - Dashboard, accounts, transactions, categories, imports, and more Deployment: - Docker Compose (PostgreSQL 17 + .NET API + nginx frontend) - Auto-migration on startup Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<v-navigation-drawer v-model="drawer" :rail="rail" permanent>
|
||||
<v-list-item
|
||||
prepend-icon="mdi-cat"
|
||||
title="Purrse"
|
||||
subtitle="Personal Finance"
|
||||
nav
|
||||
>
|
||||
<template v-slot:append>
|
||||
<v-btn
|
||||
:icon="rail ? 'mdi-chevron-right' : 'mdi-chevron-left'"
|
||||
variant="text"
|
||||
@click="rail = !rail"
|
||||
/>
|
||||
</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"
|
||||
rounded="xl"
|
||||
/>
|
||||
</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-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-btn icon="mdi-bell-outline" />
|
||||
<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-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>
|
||||
</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'
|
||||
|
||||
const drawer = ref(true)
|
||||
const rail = ref(false)
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const theme = useTheme()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
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: 'Budgets', icon: 'mdi-calculator', route: '/budgets' },
|
||||
{ title: 'Scheduled', icon: 'mdi-clock-outline', route: '/scheduled' },
|
||||
{ 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: 'Plugins', icon: 'mdi-puzzle', route: '/plugins' },
|
||||
]
|
||||
|
||||
const currentPageTitle = computed(() => {
|
||||
const item = navItems.find(n => n.route === route.path)
|
||||
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 handleLogout() {
|
||||
authStore.logout()
|
||||
router.push('/login')
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user