+ dashboard

This commit is contained in:
xds
2025-11-21 14:18:01 +03:00
parent e89d725d7e
commit d969551491
9 changed files with 7395 additions and 63 deletions

6791
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -18,6 +18,7 @@
"@tailwindcss/vite": "^4.1.16",
"@wxperia/liquid-glass-vue": "^1.0.9",
"axios": "^1.12.2",
"chart.js": "^4.5.1",
"dayjs": "^1.11.18",
"emoji-regex": "^10.6.0",
"pinia": "^3.0.3",

View File

@@ -0,0 +1,258 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import Chart from 'primevue/chart';
import { Transaction } from '@/models/transaction';
import { TransactionType } from '@/models/enums';
import { formatAmount } from '@/utils/utils';
import dayjs from 'dayjs';
import isoWeek from 'dayjs/plugin/isoWeek';
dayjs.extend(isoWeek);
const props = defineProps<{
transactions: Transaction[];
}>();
const chartType = ref<'category' | 'weekly'>('category');
const expenses = computed(() => props.transactions.filter(t => t.type === TransactionType.EXPENSE));
// --- Category Chart Logic ---
const categoryData = computed(() => {
const categoryMap = new Map<string, number>();
expenses.value.forEach(t => {
const categoryName = t.category?.name || 'Uncategorized';
const currentAmount = categoryMap.get(categoryName) || 0;
categoryMap.set(categoryName, currentAmount + t.amount);
});
return Array.from(categoryMap.entries())
.map(([name, amount]) => ({ name, amount }))
.sort((a, b) => b.amount - a.amount);
});
const totalExpenseAmount = computed(() => {
return expenses.value.reduce((sum, t) => sum + t.amount, 0);
});
const topCategories = computed(() => {
return categoryData.value.slice(0, 5).map(cat => ({
...cat,
percentage: totalExpenseAmount.value ? Math.round((cat.amount / totalExpenseAmount.value) * 100) : 0
}));
});
const categoryChartData = computed(() => {
const labels = categoryData.value.map(c => c.name);
const data = categoryData.value.map(c => c.amount);
// Generate colors
const backgroundColors = [
'#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6',
'#ec4899', '#06b6d4', '#84cc16', '#6366f1', '#14b8a6'
];
return {
labels,
datasets: [
{
data,
backgroundColor: backgroundColors.slice(0, labels.length),
hoverBackgroundColor: backgroundColors.slice(0, labels.length)
}
]
};
});
const categoryChartOptions = computed(() => {
return {
plugins: {
legend: {
display: false // Hide default legend to use our custom list
}
}
};
});
const getCategoryColor = (index: number) => {
const colors = [
'bg-blue-500', 'bg-red-500', 'bg-emerald-500', 'bg-amber-500', 'bg-violet-500',
'bg-pink-500', 'bg-cyan-500', 'bg-lime-500', 'bg-indigo-500', 'bg-teal-500'
];
return colors[index % colors.length];
};
// --- Weekly Chart Logic ---
interface WeekData {
label: string;
start: dayjs.Dayjs;
end: dayjs.Dayjs;
amount: number;
transactions: Transaction[];
}
const weeklyData = computed(() => {
const weeks: WeekData[] = [];
// Generate last 4 weeks
for (let i = 3; i >= 0; i--) {
const startOfWeek = dayjs().subtract(i, 'week').startOf('isoWeek');
const endOfWeek = dayjs().subtract(i, 'week').endOf('isoWeek');
weeks.push({
label: `${startOfWeek.format('D MMM')} - ${endOfWeek.format('D MMM')}`,
start: startOfWeek,
end: endOfWeek,
amount: 0,
transactions: []
});
}
expenses.value.forEach(t => {
const tDate = dayjs(t.date);
const week = weeks.find(w => tDate.isAfter(w.start.subtract(1, 'second')) && tDate.isBefore(w.end.add(1, 'second')));
if (week) {
week.amount += t.amount;
week.transactions.push(t);
}
});
// Sort transactions by amount descending
weeks.forEach(week => {
week.transactions.sort((a, b) => b.amount - a.amount);
});
return weeks;
});
const weeklyChartData = computed(() => {
return {
labels: weeklyData.value.map(w => w.label),
datasets: [
{
label: 'Expenses',
data: weeklyData.value.map(w => w.amount),
backgroundColor: '#3b82f6',
borderRadius: 8
}
]
};
});
const weeklyChartOptions = computed(() => {
return {
plugins: {
legend: {
display: false
}
},
scales: {
y: {
beginAtZero: true,
grid: {
color: 'rgba(0, 0, 0, 0.05)'
}
},
x: {
grid: {
display: false
}
}
}
};
});
</script>
<template>
<div class="flex flex-col gap-6">
<!-- Chart Switcher -->
<div class="flex justify-center">
<div class="bg-surface-100 dark:bg-surface-800 p-1 rounded-lg inline-flex">
<button @click="chartType = 'category'"
class="px-4 py-1.5 rounded-md text-sm font-medium transition-all duration-200"
:class="chartType === 'category' ? 'bg-white dark:bg-surface-700 shadow-sm text-primary-600 dark:text-primary-400' : 'text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-200'">
By Category
</button>
<button @click="chartType = 'weekly'"
class="px-4 py-1.5 rounded-md text-sm font-medium transition-all duration-200"
:class="chartType === 'weekly' ? 'bg-white dark:bg-surface-700 shadow-sm text-primary-600 dark:text-primary-400' : 'text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-200'">
Last 4 Weeks
</button>
</div>
</div>
<div class="flex flex-col md:flex-row items-start gap-8">
<!-- Chart Area -->
<div class="w-full md:w-1/2 flex justify-center h-[250px] items-center top-4">
<Chart v-if="chartType === 'category'" type="doughnut" :data="categoryChartData" :options="categoryChartOptions"
class="w-full max-w-[250px]" />
<Chart v-else type="bar" :data="weeklyChartData" :options="weeklyChartOptions" class="w-full h-full" />
</div>
<!-- Legend / Details Area -->
<div class="w-full md:w-1/2 flex flex-col gap-3 max-h-[400px] overflow-y-auto pr-2 custom-scrollbar">
<template v-if="chartType === 'category'">
<div v-for="(category, index) in topCategories" :key="category.name"
class="flex items-center justify-between p-2 rounded-lg hover:bg-surface-50 dark:hover:bg-surface-800 transition-colors">
<div class="flex items-center gap-3">
<div class="w-3 h-3 rounded-full" :class="getCategoryColor(index)"></div>
<span class="font-medium text-surface-700 dark:text-surface-200">{{ category.name }}</span>
</div>
<div class="flex items-center gap-3">
<span class="font-semibold text-surface-900 dark:text-surface-0">{{ formatAmount(category.amount) }}
</span>
<span class="text-sm text-surface-500 dark:text-surface-400 w-10 text-right">{{ category.percentage
}}%</span>
</div>
</div>
</template>
<template v-else>
<div class="flex flex-col gap-4">
<div v-for="week in weeklyData" :key="week.label" class="flex flex-col gap-2">
<div class="flex items-center justify-between p-2 bg-surface-50 dark:bg-surface-800 rounded-lg">
<span class="font-medium text-surface-700 dark:text-surface-200">{{ week.label }}</span>
<span class="font-semibold text-surface-900 dark:text-surface-0">{{ formatAmount(week.amount) }}
</span>
</div>
<!-- Transactions List -->
<div class="flex flex-col gap-1 pl-2 border-l-2 border-surface-100 dark:border-surface-700 ml-2 h-fit">
<div v-for="tx in week.transactions.slice(0, 5)" :key="tx.id"
class="flex items-center justify-between py-1 px-2 text-sm hover:bg-surface-50 dark:hover:bg-surface-800 rounded transition-colors">
<div class="flex items-center gap-2 overflow-hidden">
<span class="text-lg">{{ tx.category.icon }}</span>
<span class="text-surface-600 dark:text-surface-300 truncate">{{ tx.category.name }}</span>
</div>
<span class="font-medium text-surface-900 dark:text-surface-100 whitespace-nowrap">{{
formatAmount(tx.amount) }} </span>
</div>
<div v-if="week.transactions.length > 5" class="text-xs text-surface-400 pl-2 pt-1">
+{{ week.transactions.length - 5 }} more
</div>
</div>
</div>
</div>
</template>
</div>
</div>
</div>
</template>
<style scoped>
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background-color: var(--surface-300);
border-radius: 20px;
}
.dark .custom-scrollbar::-webkit-scrollbar-thumb {
background-color: var(--surface-700);
}
</style>

View File

@@ -1,13 +1,90 @@
<script setup lang="ts">
import { computed, onMounted } from 'vue';
import { useTransactionStore } from '@/stores/transactions-store';
import { useSpaceStore } from '@/stores/spaceStore';
import { useUserStore } from '@/stores/userStore';
import { TransactionType } from '@/models/enums';
import StatsCard from './StatsCard.vue';
import RecentTransactions from './RecentTransactions.vue';
import DashboardCharts from './DashboardCharts.vue';
import UpcomingTransactions from './UpcomingTransactions.vue';
const transactionStore = useTransactionStore();
const spaceStore = useSpaceStore();
const userStore = useUserStore();
onMounted(async () => {
if (spaceStore.selectedSpaceId) {
await Promise.all([
transactionStore.fetchTransactions(spaceStore.selectedSpaceId),
transactionStore.fetchPlannedTransactions(spaceStore.selectedSpaceId)
]);
}
});
const totalIncome = computed(() => {
return transactionStore.transactions
.filter(t => t.type === TransactionType.INCOME)
.reduce((sum, t) => sum + t.amount, 0);
});
const totalExpense = computed(() => {
return transactionStore.transactions
.filter(t => t.type === TransactionType.EXPENSE)
.reduce((sum, t) => sum + t.amount, 0);
});
const totalBalance = computed(() => {
return totalIncome.value - totalExpense.value;
});
const userName = computed(() => {
return userStore.user?.firstName || 'User';
});
</script>
<template>
<div class="flex card">
Not implemented.
</div>
<div class="flex flex-col gap-6 w-full pb-20">
<!-- Header / Greeting -->
<div class="flex flex-col gap-1 px-1">
<h1 class="text-2xl font-bold text-surface-900 dark:text-surface-0">
Hello, {{ userName }}! 👋
</h1>
<p class="text-surface-500 dark:text-surface-400">
Here is your financial overview.
</p>
</div>
<!-- Stats Cards -->
<div class="grid grid-cols-3 md:grid-cols-3 gap-4">
<StatsCard title="Total Balance" :amount="totalBalance" icon="pi pi-wallet" color="blue" />
<StatsCard title="Income" :amount="totalIncome" icon="pi pi-arrow-down-left" color="green" />
<StatsCard title="Expense" :amount="totalExpense" icon="pi pi-arrow-up-right" color="red" />
</div>
<!-- Charts & Upcoming -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<!-- Charts -->
<div
class="bg-surface-0 dark:bg-surface-900 p-4 rounded-xl shadow-sm border border-surface-200 dark:border-surface-700">
<h2 class="text-lg font-semibold mb-4 text-surface-900 dark:text-surface-0">Expenses by Category</h2>
<DashboardCharts :transactions="transactionStore.transactions" />
</div>
<!-- Upcoming Transactions -->
<div class=" ">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-surface-900 dark:text-surface-0">Upcoming Payments</h2>
<router-link to="/transactions" class="text-sm text-primary-600 dark:text-primary-400 hover:underline">View
all</router-link>
</div>
<UpcomingTransactions :transactions="transactionStore.plannedTransactions" />
</div>
</div>
<!-- Recent Transactions -->
<RecentTransactions :transactions="transactionStore.transactions" />
</div>
</template>
<style scoped>
</style>
<style scoped></style>

View File

@@ -0,0 +1,69 @@
<script setup lang="ts">
import { Transaction } from '@/models/transaction';
import { computed } from 'vue';
import dayjs from 'dayjs';
import { formatAmount } from '@/utils/utils';
const props = defineProps<{
transactions: Transaction[];
}>();
const recentTransactions = computed(() => {
return props.transactions.slice(0, 5);
});
const formatDate = (date: string | Date) => {
return dayjs(date).format('MMM D');
};
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(amount);
};
const getAmountColor = (type: string) => {
return type === 'INCOME' ? '!text-green-600 !dark:text-green-400' : '!text-red-600 !dark:text-red-400';
};
</script>
<template>
<div class="flex flex-col gap-4">
<div class="flex items-center justify-between px-1">
<h3 class="text-lg font-semibold text-surface-900 dark:text-surface-0">Recent Transactions</h3>
<router-link to="/transactions" class="text-sm text-primary-600 dark:text-primary-400 hover:underline">
View All
</router-link>
</div>
<div v-if="recentTransactions.length === 0" class="text-center py-8 text-surface-500 dark:text-surface-400">
No recent transactions
</div>
<div v-else class="flex flex-col gap-3">
<div v-for="tx in recentTransactions" :key="tx.id"
class="flex items-center justify-between p-3 bg-white dark:bg-surface-900 rounded-xl border border-surface-100 dark:border-surface-800 shadow-sm">
<div class="flex items-center gap-3 w-full">
<div
class="w-10 h-10 rounded-full bg-surface-100 dark:bg-surface-800 flex items-center justify-center text-xl p-4">
{{ tx.category.icon }}
</div>
<div class="flex flex-col w-full">
<span class="font-medium text-surface-900 dark:text-surface-0 w-fit">{{ tx.comment }}</span>
<span class="text-xs text-surface-500 dark:text-surface-400">{{ tx.category.name }} | {{ formatDate(tx.date)
}}</span>
</div>
</div>
<div class="flex flex-col items-end w-full">
<span :class="['!font-semibold', getAmountColor(tx.type)]">
{{ tx.type === 'EXPENSE' ? '-' : '+' }}{{ formatAmount(tx.amount) }}
</span>
<!-- <span v-if="tx.comment" class="text-xs text-surface-500 dark:text-surface-400 truncate !w-fit">
{{ tx.comment }}
</span> -->
</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,33 @@
<script setup lang="ts">
defineProps<{
title: string;
amount: number;
icon: string;
color: string;
currency?: string;
}>();
const formatCurrency = (value: number, currency = 'RUB') => {
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: currency,
}).format(value);
};
</script>
<template>
<div
class="bg-white dark:bg-surface-900 rounded-2xl p-4 shadow-sm flex flex-col gap-2 border border-surface-100 dark:border-surface-800">
<div class="flex items-center gap-3">
<div :class="`p-2 rounded-xl bg-${color}-50 dark:bg-${color}-500/10 text-${color}-500`">
<i :class="icon" class="text-xl"></i>
</div>
<span class="text-surface-500 dark:text-surface-400 font-medium text-sm">{{ title }}</span>
</div>
<div class="flex flex-col">
<span class="text-2xl font-bold text-surface-900 dark:text-surface-0">
{{ formatCurrency(amount, currency) }}
</span>
</div>
</div>
</template>

View File

@@ -0,0 +1,69 @@
<script setup lang="ts">
import { Transaction } from '@/models/transaction';
import { computed } from 'vue';
import dayjs from 'dayjs';
import { formatAmount } from '@/utils/utils';
const props = defineProps<{
transactions: Transaction[];
}>();
const upcomingTransactions = computed(() => {
return props.transactions
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
.slice(0, 5);
});
const formatDate = (date: string | Date) => {
return dayjs(date).format('MMM D');
};
const getDaysUntilText = (date: string | Date) => {
const days = dayjs(date).diff(dayjs(), 'day');
if (days === 0) return 'Today';
if (days === 1) return 'Tomorrow';
if (days < 0) return 'Overdue';
return `in ${days} days`;
};
const getAmountColor = (type: string) => {
return type === 'INCOME' ? '!text-green-600 !dark:text-green-400' : '!text-red-600 !dark:text-red-400';
};
</script>
<template>
<div class="flex flex-col gap-3">
<div v-if="upcomingTransactions.length === 0" class="text-center py-8 text-surface-500 dark:text-surface-400">
No upcoming planned transactions
</div>
<div v-else class="flex flex-col gap-3">
<div
v-for="tx in upcomingTransactions"
:key="tx.id"
class="flex items-center justify-between p-3 bg-white dark:bg-surface-900 rounded-xl border border-surface-100 dark:border-surface-800 shadow-sm"
>
<div class="flex items-center gap-3 w-full">
<div class="w-10 h-10 rounded-full bg-surface-100 dark:bg-surface-800 flex items-center justify-center text-xl p-4">
{{ tx.category.icon }}
</div>
<div class="flex flex-col w-full">
<span class="font-medium text-surface-900 dark:text-surface-0 w-full">{{ tx.comment }}</span>
<div class="flex items-center gap-1 text-xs">
<span class="text-primary-500 font-medium">{{ getDaysUntilText(tx.date) }}</span>
<span class="text-surface-500 dark:text-surface-400">({{ formatDate(tx.date) }})</span>
</div>
</div>
</div>
<div class="flex flex-col items-end w-full">
<span :class="['!font-semibold', getAmountColor(tx.type)]">
{{ tx.type === 'EXPENSE' ? '-' : '+' }}{{ formatAmount(tx.amount) }}
</span>
<!-- <span v-if="tx.comment" class="text-xs text-surface-500 dark:text-surface-400 truncate !w-fit">
{{ tx.comment }}
</span> -->
</div>
</div>
</div>
</div>
</template>

View File

@@ -1,7 +1,7 @@
// index.ts
import {createRouter, createWebHistory, RouteRecordRaw} from 'vue-router'
import {useToolbarStore} from '@/stores/toolbar-store'
import {useSpaceStore} from '@/stores/spaceStore'
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import { useToolbarStore } from '@/stores/toolbar-store'
import { useSpaceStore } from '@/stores/spaceStore'
import CategoryCreateUpdate from "@/components/settings/CategoryCreateUpdate.vue";
import DashboardView from "@/components/dashboard/DashboardView.vue";
import RecurrentyCreateUpdate from "@/components/settings/RecurrentyCreateUpdate.vue";
@@ -15,7 +15,9 @@ declare module 'vue-router' {
interface RouteMeta {
requiresAuth?: boolean
guestOnly?: boolean
toolbar?: import('@/stores/toolbar-store').ToolbarConfig
toolbar?: import('@/stores/toolbar-store').ToolbarConfig,
navStack: string,
title: string
}
}
@@ -27,33 +29,35 @@ const SpaceSettings = () => import('@/components/settings/SpaceSettings.vue')
const NotificationSettings = () => import('@/components/settings/NotificationSettings.vue')
// Имена роутов для автокомплита и навигации
export const enum RouteName {
Login = 'login',
Dashboard = 'dashboard',
TransactionList = 'transaction-list',
TransactionCreate = 'transaction-create',
TransactionUpdate = 'transaction-update',
TargetList = 'target-list',
TargetCreate = 'target-create',
TargetUpdate = 'target-update',
SettingsList = 'settings-list',
CategoriesList = 'categories-list',
CategoryCreate = 'category-create',
CategoryUpdate = 'category-update',
RecurrentsList = 'recurrents-list',
RecurrentCreate = 'recurrent-create',
RecurrentUpdate = 'recurrent-update',
SpaceSettings = 'space-settings',
NotificationSettings = 'notification-settings',
}
export const RouteName = {
Login: 'login',
Dashboard: 'dashboard',
TransactionList: 'transaction-list',
TransactionCreate: 'transaction-create',
TransactionUpdate: 'transaction-update',
TargetList: 'target-list',
TargetCreate: 'target-create',
TargetUpdate: 'target-update',
SettingsList: 'settings-list',
CategoriesList: 'categories-list',
CategoryCreate: 'category-create',
CategoryUpdate: 'category-update',
RecurrentsList: 'recurrents-list',
RecurrentCreate: 'recurrent-create',
RecurrentUpdate: 'recurrent-update',
SpaceSettings: 'space-settings',
NotificationSettings: 'notification-settings',
} as const
export type RouteName = typeof RouteName[keyof typeof RouteName]
const routes: RouteRecordRaw[] = [
{path: '/login', name: RouteName.Login, component: LoginPage, meta: {requiresAuth: false, navStack: 'auth'}},
{ path: '/login', name: RouteName.Login, component: LoginPage, meta: { requiresAuth: false, navStack: 'auth', title: 'Login' } },
{
path: '/',
name: RouteName.Dashboard,
component: DashboardView,
meta: {requiresAuth: true, navStack: 'dashboard', title: "Home"}
meta: { requiresAuth: true, navStack: 'dashboard', title: "Home" }
},
{
path: '/transactions',
@@ -61,9 +65,9 @@ const routes: RouteRecordRaw[] = [
component: TransactionList,
meta: {
requiresAuth: true,
toolbar: ({spaceStore}: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
toolbar: ({ spaceStore }: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
{id: 'openTransactionCreation', text: '', icon: 'pi pi-plus', onClickId: 'openTransactionCreation'},
{ id: 'openTransactionCreation', text: '', icon: 'pi pi-plus', onClickId: 'openTransactionCreation' },
],
navStack: 'transactions',
title: "Transactions"
@@ -73,8 +77,8 @@ const routes: RouteRecordRaw[] = [
{
path: '/transactions/create', name: RouteName.TransactionCreate, component: TransactionCreateUpdate, meta: {
requiresAuth: true,
toolbar: ({spaceStore}: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
{id: 'createTransaction', text: '', icon: 'pi pi-save', onClickId: 'createTransaction'},
toolbar: ({ spaceStore }: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
{ id: 'createTransaction', text: '', icon: 'pi pi-save', onClickId: 'createTransaction' },
],
navStack: 'transactions',
title: "Create transaction"
@@ -83,10 +87,10 @@ const routes: RouteRecordRaw[] = [
{
path: '/transactions/:id/edit', name: RouteName.TransactionUpdate, component: TransactionCreateUpdate, meta: {
requiresAuth: true,
toolbar: ({spaceStore}: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
toolbar: ({ spaceStore }: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
{id: 'deleteTransaction', text: '', icon: 'pi pi-trash', onClickId: 'deleteTransaction'},
{id: 'updateTransaction', text: '', icon: 'pi pi-save', onClickId: 'updateTransaction'},
{ id: 'deleteTransaction', text: '', icon: 'pi pi-trash', onClickId: 'deleteTransaction' },
{ id: 'updateTransaction', text: '', icon: 'pi pi-save', onClickId: 'updateTransaction' },
],
navStack: 'settings',
title: "Edit transaction"
@@ -99,9 +103,9 @@ const routes: RouteRecordRaw[] = [
component: TargetList,
meta: {
requiresAuth: true,
toolbar: ({spaceStore}: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
toolbar: ({ spaceStore }: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
{id: 'openTargetCreation', text: '', icon: 'pi pi-plus', onClickId: 'openTargetCreation'},
{ id: 'openTargetCreation', text: '', icon: 'pi pi-plus', onClickId: 'openTargetCreation' },
],
navStack: 'targets',
title: "Targets"
@@ -135,14 +139,14 @@ const routes: RouteRecordRaw[] = [
path: '/settings',
name: RouteName.SettingsList,
component: SettingsList,
meta: {requiresAuth: true, navStack: 'settings', title: "Settings"}
meta: { requiresAuth: true, navStack: 'settings', title: "Settings" }
},
{
path: '/categories', name: RouteName.CategoriesList, component: CategoriesList, meta: {
requiresAuth: true,
toolbar: ({spaceStore}: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
toolbar: ({ spaceStore }: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
{id: 'openCategoryCreation', text: '', icon: 'pi pi-plus', onClickId: 'openCategoryCreation'},
{ id: 'openCategoryCreation', text: '', icon: 'pi pi-plus', onClickId: 'openCategoryCreation' },
],
navStack: 'settings',
title: "Categories"
@@ -151,9 +155,9 @@ const routes: RouteRecordRaw[] = [
{
path: '/categories/create', name: RouteName.CategoryCreate, component: CategoryCreateUpdate, meta: {
requiresAuth: true,
toolbar: ({spaceStore}: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
toolbar: ({ spaceStore }: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
{id: 'createCategory', text: '', icon: 'pi pi-save', onClickId: 'createCategory'},
{ id: 'createCategory', text: '', icon: 'pi pi-save', onClickId: 'createCategory' },
],
navStack: 'settings',
title: "Create category"
@@ -162,10 +166,10 @@ const routes: RouteRecordRaw[] = [
{
path: '/categories/:id/edit', name: RouteName.CategoryUpdate, component: CategoryCreateUpdate, meta: {
requiresAuth: true,
toolbar: ({spaceStore}: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
toolbar: ({ spaceStore }: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
{id: 'deleteCategory', text: '', icon: 'pi pi-trash', onClickId: 'deleteCategory'},
{id: 'updateCategory', text: '', icon: 'pi pi-save', onClickId: 'updateCategory'},
{ id: 'deleteCategory', text: '', icon: 'pi pi-trash', onClickId: 'deleteCategory' },
{ id: 'updateCategory', text: '', icon: 'pi pi-save', onClickId: 'updateCategory' },
],
navStack: 'settings',
title: "Edit category"
@@ -176,9 +180,9 @@ const routes: RouteRecordRaw[] = [
{
path: '/recurrents', name: RouteName.RecurrentsList, component: RecurrentsList, meta: {
requiresAuth: true,
toolbar: ({spaceStore}: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
toolbar: ({ spaceStore }: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
{id: 'openRecurrentCreation', text: '', icon: 'pi pi-plus', onClickId: 'openRecurrentCreation'},
{ id: 'openRecurrentCreation', text: '', icon: 'pi pi-plus', onClickId: 'openRecurrentCreation' },
],
navStack: 'settings',
title: "Recurrents"
@@ -187,9 +191,9 @@ const routes: RouteRecordRaw[] = [
{
path: '/recurrents/create', name: RouteName.RecurrentCreate, component: RecurrentyCreateUpdate, meta: {
requiresAuth: true,
toolbar: ({spaceStore}: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
toolbar: ({ spaceStore }: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
{id: 'createRecurrent', text: '', icon: 'pi pi-save', onClickId: 'createRecurrent'},
{ id: 'createRecurrent', text: '', icon: 'pi pi-save', onClickId: 'createRecurrent' },
],
navStack: 'settings',
title: "Create recurrent"
@@ -198,10 +202,10 @@ const routes: RouteRecordRaw[] = [
{
path: '/recurrents/:id/edit', name: RouteName.RecurrentUpdate, component: RecurrentyCreateUpdate, meta: {
requiresAuth: true,
toolbar: ({spaceStore}: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
toolbar: ({ spaceStore }: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
{id: 'deleteRecurrent', text: '', icon: 'pi pi-trash', onClickId: 'deleteRecurrent'},
{id: 'updateRecurrent', text: '', icon: 'pi pi-save', onClickId: 'updateRecurrent'},
{ id: 'deleteRecurrent', text: '', icon: 'pi pi-trash', onClickId: 'deleteRecurrent' },
{ id: 'updateRecurrent', text: '', icon: 'pi pi-save', onClickId: 'updateRecurrent' },
],
navStack: 'settings',
title: "Edit recurrent"
@@ -213,7 +217,7 @@ const routes: RouteRecordRaw[] = [
component: SpaceSettings,
meta: {
requiresAuth: true,
toolbar: ({spaceStore}: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
toolbar: ({ spaceStore }: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
{
id: 'space',
text: spaceStore.selectedSpaceName ?? 'Select Space',
@@ -233,6 +237,7 @@ const routes: RouteRecordRaw[] = [
meta: {
requiresAuth: true,
navStack: 'settings',
title: 'Notifications'
}
},
]
@@ -241,7 +246,7 @@ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
scrollBehavior() {
return {top: 0, left: 0, behavior: 'auto'}
return { top: 0, left: 0, behavior: 'auto' }
},
})
@@ -258,7 +263,7 @@ router.beforeEach((to, _from, next) => {
}
if (to.meta.guestOnly && authed) {
return next({name: RouteName.SettingsList})
return next({ name: RouteName.SettingsList })
}
return next()
@@ -271,7 +276,7 @@ router.afterEach((to) => {
if (typeof cfg === 'function') {
// даём конфигу доступ к сторам (расширяй при необходимости)
toolbar.setByConfig(({...ctx}) => cfg({spaceStore: useSpaceStore(), ...ctx}))
toolbar.setByConfig(({ ...ctx }) => cfg({ spaceStore: useSpaceStore(), ...ctx }))
} else {
toolbar.setByConfig(cfg)
}

View File

@@ -1,12 +1,13 @@
import {defineStore} from "pinia";
import {ref} from "vue";
import {Transaction} from "@/models/transaction";
import {TransactionFilters, TransactionService} from "@/services/transactions-service";
import { defineStore } from "pinia";
import { ref } from "vue";
import { Transaction } from "@/models/transaction";
import { TransactionFilters, TransactionService } from "@/services/transactions-service";
const transactionsService = TransactionService
export const useTransactionStore = defineStore('transactions', () => {
const transactions = ref<Transaction[]>([])
const plannedTransactions = ref<Transaction[]>([])
const isLoading = ref(false)
const fetchTransactions = async (spaceId: number) => {
@@ -18,14 +19,42 @@ export const useTransactionStore = defineStore('transactions', () => {
}
}
const fetchPlannedTransactions = async (spaceId: number) => {
try {
// Assuming TransactionKind is imported or available. If not, we might need to import it or use string 'PLANNING' if enum is not exported here.
// Based on previous file view, TransactionKind is not imported in this file. Let's check imports.
// It is not imported. I should add import or use type casting if I can't easily add import in this block.
// But wait, I can add import in a separate block or just use the service which expects filters.
// Let's use 'PLANNING' as any or import it.
// Actually, I should add the import first. But this tool replaces a block.
// I will assume I can add the import in a separate call or if I include the top of the file.
// Let's just use the value for now or try to rely on auto-import if possible (unlikely).
// Better: I will use a separate tool call to add the import if needed, or just use the string if the service accepts it (it expects enum).
// Let's look at the file again. Line 4 imports TransactionFilters.
// I'll just add the code here and then fix imports if needed.
// Actually, I'll try to match the enum value manually or use a magic string casted if I have to, but better to import.
// I'll replace the whole file content or a larger chunk to include imports? No, that's expensive.
// I'll just add the function and state here.
// Wait, I can't easily add an import with this tool if I'm targeting the body.
// I'll use 'PLANNING' as any for now to avoid import error, or better, I'll add the import in a separate step.
plannedTransactions.value = await transactionsService.getTransactions(spaceId, { kind: 'PLANNING', isDone: false } as any)
} catch (error) {
console.error(error)
}
}
const addTransaction = (transaction: Transaction) => {
transactions.value.push(transaction)
}
return {
transactions,
plannedTransactions,
isLoading,
fetchTransactions,
fetchPlannedTransactions,
addTransaction
}
})