feat: Add category transactions modal and expandable category list to the dashboard.
This commit is contained in:
67
src/App.vue
67
src/App.vue
@@ -3,13 +3,13 @@ import SpaceList from "@/components/space-list/SpaceList.vue";
|
||||
import Toolbar from "@/components/Toolbar.vue";
|
||||
import Toast from "primevue/toast";
|
||||
import ProgressSpinner from "primevue/progressspinner";
|
||||
import {useSpaceStore} from "@/stores/spaceStore";
|
||||
import {useToolbarStore} from "@/stores/toolbar-store";
|
||||
import { useSpaceStore } from "@/stores/spaceStore";
|
||||
import { useToolbarStore } from "@/stores/toolbar-store";
|
||||
import router from "@/router";
|
||||
import {useRoute} from "vue-router";
|
||||
import {computed, onBeforeUnmount, onMounted, ref, watch} from "vue";
|
||||
import {useToast} from "primevue/usetoast";
|
||||
import {useUserStore} from "@/stores/userStore";
|
||||
import { useRoute } from "vue-router";
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import { useToast } from "primevue/usetoast";
|
||||
import { useUserStore } from "@/stores/userStore";
|
||||
|
||||
const spaceStore = useSpaceStore();
|
||||
const toolbarStore = useToolbarStore();
|
||||
@@ -25,13 +25,13 @@ const isTelegram = computed(() => !!tgApp);
|
||||
|
||||
const isSpaceSelectorVisible = ref(false);
|
||||
const isSpaceSelected = computed(
|
||||
() => spaceStore.selectedSpaceId === undefined || isSpaceSelectorVisible.value
|
||||
() => spaceStore.selectedSpaceId === undefined || isSpaceSelectorVisible.value
|
||||
);
|
||||
|
||||
const menu = [
|
||||
{name: "Dashboard", icon: "pi pi-chart-bar", link: "/", navStack: 'dashboard'},
|
||||
{name: "Transactions", icon: "pi pi-list", link: "/transactions", navStack: 'transactions'},
|
||||
{name: "Settings", icon: "pi pi-cog", link: "/settings", navStack: 'settings'},
|
||||
{ name: "Dashboard", icon: "pi pi-chart-bar", link: "/", navStack: 'dashboard' },
|
||||
{ name: "Transactions", icon: "pi pi-list", link: "/transactions", navStack: 'transactions' },
|
||||
{ name: "Settings", icon: "pi pi-cog", link: "/settings", navStack: 'settings' },
|
||||
];
|
||||
|
||||
function spaceSelected() {
|
||||
@@ -124,8 +124,8 @@ onMounted(async () => {
|
||||
|
||||
// 🔁 следим за изменением маршрута
|
||||
watch(
|
||||
() => route.path,
|
||||
() => setupBackButton()
|
||||
() => route.path,
|
||||
() => setupBackButton()
|
||||
);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
@@ -138,54 +138,43 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Toast/>
|
||||
<Toast />
|
||||
<div v-if="isLoading">
|
||||
<ProgressSpinner/>
|
||||
<ProgressSpinner />
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div v-if="!userStore.isAuthorized">
|
||||
<router-view/>
|
||||
<router-view />
|
||||
</div>
|
||||
<div v-else class="flex flex-col tg " :class="['ios', 'android'].includes(platform) ? '!pt-10' : ''">
|
||||
<SpaceList v-if="isSpaceSelected && userStore.isAuthorized" @space-selected="spaceSelected"/>
|
||||
<SpaceList v-if="isSpaceSelected && userStore.isAuthorized" @space-selected="spaceSelected" />
|
||||
|
||||
<div v-else class="flex flex-col w-full gap-4">
|
||||
<div class="flex w-full flex flex-row items-end justify-end pt-2 pe-4">
|
||||
<Toolbar/>
|
||||
<Toolbar />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col w-full h-full items-end px-4 gap-4 pb-6">
|
||||
<router-view class=" w-full"/>
|
||||
<router-view class=" w-full" />
|
||||
</div>
|
||||
<button
|
||||
v-if="isInputFocused"
|
||||
@click="blurAllInputs"
|
||||
class="fixed bottom-4 right-4 z-50 bg-blue-500 text-white px-4 py-2 rounded-lg shadow-lg"
|
||||
>
|
||||
<button v-if="isInputFocused" @click="blurAllInputs"
|
||||
class="fixed bottom-4 right-4 z-50 bg-blue-500 text-white px-4 py-2 rounded-lg shadow-lg">
|
||||
Готово
|
||||
</button>
|
||||
<nav v-if="isNavVisible"
|
||||
class="fixed inset-x-0 bottom-4 z-50 w-full flex justify-center items-center "
|
||||
style="padding-bottom: var(--tg-content-safe-area-inset-bottom) !important;"
|
||||
>
|
||||
|
||||
<div class="flex h-full items-center justify-between py-2 bg-white rounded-4xl px-6 w-fit shadow">
|
||||
<!-- <div class="flex h-full justify-items-center items-center justify-between py-2 bg-white rounded-4xl !px-6 w-fit">-->
|
||||
<router-link
|
||||
v-for="item in menu"
|
||||
:key="item.link"
|
||||
:to="item.link"
|
||||
class="flex w-fit h-full flex-col items-center gap-2 !py-2 !px-4"
|
||||
:class="route.meta.navStack === item.navStack ? 'bg-green-100 rounded-2xl ' : ''"
|
||||
>
|
||||
<i class="!text-lg" :class="item.icon"/>
|
||||
<nav v-if="isNavVisible" class="fixed bottom-4 left-1/2 -translate-x-1/2 z-50"
|
||||
style="padding-bottom: var(--tg-content-safe-area-inset-bottom) !important;">
|
||||
<div class="flex items-center justify-between py-2 bg-white rounded-4xl px-6 shadow">
|
||||
<router-link v-for="item in menu" :key="item.link" :to="item.link"
|
||||
class="flex w-fit h-full flex-col items-center gap-2 !py-2 !px-4"
|
||||
:class="route.meta.navStack === item.navStack ? 'bg-green-100 rounded-2xl ' : ''">
|
||||
<i class="!text-lg" :class="item.icon" />
|
||||
<span class="font-medium text-gray-900">{{ item.name }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="flex h-16"/>
|
||||
<div class="flex h-16" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -164,7 +164,7 @@ body {
|
||||
width: 100% !important;
|
||||
background-color: var(--surface-ground);
|
||||
color: var(--text-color);
|
||||
font-family: 'Inter', sans-serif;
|
||||
/* font-family: 'Inter', sans-serif; */
|
||||
}
|
||||
|
||||
/* Checkbox */
|
||||
@@ -182,11 +182,17 @@ body {
|
||||
border-radius: 1rem;
|
||||
}
|
||||
.p-button-rounded{
|
||||
color: gray !important;
|
||||
background-color: white !important;
|
||||
}
|
||||
.p-button-rounded:hover{
|
||||
color: white !important;
|
||||
background-color: lightgray !important;
|
||||
}
|
||||
.p-menu .p-menuitem{
|
||||
color: gray !important;
|
||||
}
|
||||
|
||||
|
||||
.p-menu .p-menuitem:hover {
|
||||
background-color: var(--menu-item-hover-bg-color) !important;
|
||||
|
||||
127
src/components/dashboard/CategoryTransactionsModal.vue
Normal file
127
src/components/dashboard/CategoryTransactionsModal.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from 'vue';
|
||||
import Dialog from 'primevue/dialog';
|
||||
import { Transaction } from '@/models/transaction';
|
||||
import { TransactionService, TransactionFilters } from '@/services/transactions-service';
|
||||
import { useSpaceStore } from '@/stores/spaceStore';
|
||||
import { formatAmount, toDateOnly } from '@/utils/utils';
|
||||
import ProgressSpinner from 'primevue/progressspinner';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
categoryId: number | null;
|
||||
categoryName: string;
|
||||
categoryIcon: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean];
|
||||
}>();
|
||||
|
||||
const spaceStore = useSpaceStore();
|
||||
const transactions = ref<Transaction[]>([]);
|
||||
const isLoading = ref(false);
|
||||
|
||||
const dialogVisible = computed({
|
||||
get: () => props.visible,
|
||||
set: (value) => emit('update:visible', value)
|
||||
});
|
||||
|
||||
const fetchCategoryTransactions = async () => {
|
||||
if (!props.categoryId || !spaceStore.selectedSpaceId) return;
|
||||
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const filters: TransactionFilters = {
|
||||
type: null,
|
||||
kind: null,
|
||||
categoriesIds: [props.categoryId],
|
||||
dateFrom: toDateOnly(props.startDate),
|
||||
dateTo: toDateOnly(props.endDate),
|
||||
isDone: null,
|
||||
offset: 0,
|
||||
limit: 100,
|
||||
sorts: [{ "sortBy": "t.date", "sortDirection": "DESC" }, { "sortBy": "t.id", "sortDirection": "DESC" }]
|
||||
};
|
||||
|
||||
transactions.value = await TransactionService.getTransactions(spaceStore.selectedSpaceId, filters);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch category transactions:', error);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
watch(() => props.visible, (newVal) => {
|
||||
if (newVal && props.categoryId) {
|
||||
fetchCategoryTransactions();
|
||||
}
|
||||
});
|
||||
|
||||
const getTransactionTypeColor = (type: string) => {
|
||||
return type === 'EXPENSE' ? 'text-red-600' : 'text-green-600';
|
||||
};
|
||||
|
||||
const getTransactionTypeIcon = (type: string) => {
|
||||
return type === 'EXPENSE' ? 'pi pi-arrow-up-right' : 'pi pi-arrow-down-left';
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model:visible="dialogVisible" :header="`${categoryIcon} ${categoryName}`" :modal="true"
|
||||
:style="{ width: '90vw', maxWidth: '600px' }" :dismissableMask="true">
|
||||
<div v-if="isLoading" class="flex justify-center py-8">
|
||||
<ProgressSpinner />
|
||||
</div>
|
||||
|
||||
<div v-else-if="transactions.length === 0" class="text-center py-8 text-surface-500">
|
||||
No transactions found for this category
|
||||
</div>
|
||||
|
||||
<div v-else class="flex flex-col gap-2 max-h-[60vh] overflow-y-auto custom-scrollbar">
|
||||
<div v-for="transaction in transactions" :key="transaction.id"
|
||||
class="flex items-center justify-between p-3 rounded-lg hover:bg-surface-50 dark:hover:bg-surface-800 transition-colors border border-surface-200 dark:border-surface-700">
|
||||
<div class="flex flex-col gap-1 flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<i
|
||||
:class="[getTransactionTypeIcon(transaction.type), getTransactionTypeColor(transaction.type)]"></i>
|
||||
<span class="font-medium text-surface-900 dark:text-surface-0">
|
||||
{{ transaction.comment || 'No description' }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-xs text-surface-500 dark:text-surface-400">
|
||||
{{ dayjs(transaction.date).format('DD MMM YYYY') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1 font-semibold" :class="getTransactionTypeColor(transaction.type)">
|
||||
<span>{{ transaction.type === 'EXPENSE' ? '-' : '+' }}</span>
|
||||
<span>{{ formatAmount(transaction.amount) }}</span>
|
||||
<span>₽</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</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>
|
||||
@@ -1,6 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import Chart from 'primevue/chart';
|
||||
import Button from 'primevue/button';
|
||||
import CategoryTransactionsModal from './CategoryTransactionsModal.vue';
|
||||
import { DashboardCategory, DashboardWeek } from '@/models/dashboard';
|
||||
import { Transaction } from '@/models/transaction';
|
||||
import { TransactionType } from '@/models/enums';
|
||||
@@ -14,9 +16,25 @@ const props = defineProps<{
|
||||
categories: DashboardCategory[];
|
||||
transactions: Transaction[];
|
||||
weeks: DashboardWeek[];
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
}>();
|
||||
|
||||
const chartType = ref<'category' | 'weekly'>('category');
|
||||
const isCategoriesExpanded = ref(false);
|
||||
|
||||
// Modal state
|
||||
const isModalVisible = ref(false);
|
||||
const selectedCategory = ref<{ id: number; name: string; icon: string } | null>(null);
|
||||
|
||||
const openCategoryModal = (category: DashboardCategory) => {
|
||||
selectedCategory.value = {
|
||||
id: category.category.id,
|
||||
name: category.category.name,
|
||||
icon: category.category.icon
|
||||
};
|
||||
isModalVisible.value = true;
|
||||
};
|
||||
|
||||
const expenses = computed(() => props.categories.filter(c => c.category.type === TransactionType.EXPENSE));
|
||||
|
||||
@@ -24,11 +42,9 @@ const expenses = computed(() => props.categories.filter(c => c.category.type ===
|
||||
const categoryData = computed(() => {
|
||||
return expenses.value
|
||||
.map(c => ({
|
||||
...c,
|
||||
name: c.category.name,
|
||||
amount: c.currentPeriodAmount,
|
||||
currentPeriodAmount: c.currentPeriodAmount,
|
||||
previousPeriodAmount: c.previousPeriodAmount,
|
||||
changeDiffPercentage: c.changeDiffPercentage
|
||||
}))
|
||||
.sort((a, b) => b.amount - a.amount);
|
||||
});
|
||||
@@ -38,12 +54,16 @@ const totalExpenseAmount = computed(() => {
|
||||
});
|
||||
|
||||
const topCategories = computed(() => {
|
||||
return categoryData.value.slice(0, 5).map(cat => ({
|
||||
return categoryData.value.map(cat => ({
|
||||
...cat,
|
||||
percentage: totalExpenseAmount.value ? Math.round((cat.amount / totalExpenseAmount.value) * 100) : 0
|
||||
}));
|
||||
});
|
||||
|
||||
const displayedCategories = computed(() => {
|
||||
return isCategoriesExpanded.value ? topCategories.value : topCategories.value.slice(0, 5);
|
||||
});
|
||||
|
||||
const categoryChartData = computed(() => {
|
||||
const labels = categoryData.value.map(c => c.name);
|
||||
const data = categoryData.value.map(c => c.amount);
|
||||
@@ -157,36 +177,46 @@ const weeklyChartOptions = computed(() => {
|
||||
</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">
|
||||
<div class="w-full md:w-1/2 flex flex-col gap-3">
|
||||
<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">
|
||||
<div class="flex items-center font-semibold gap-1 text-surface-900 dark:text-surface-0">
|
||||
<div class="flex items-center gap-0">
|
||||
<span>{{ formatAmount(category.amount) }}</span>
|
||||
<span class="ml-0.5">₽</span>
|
||||
<div class="categories-wrapper">
|
||||
<div :class="['categories-content', { 'expanded': isCategoriesExpanded }]">
|
||||
<div v-for="(category, index) in displayedCategories" :key="category.name"
|
||||
class="flex items-center justify-between p-2 rounded-lg hover:bg-surface-50 dark:hover:bg-surface-800 transition-colors cursor-pointer"
|
||||
@click="openCategoryModal(category)">
|
||||
<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>
|
||||
<span class="text-xs text-surface-500 dark:text-surface-400 ml-1"> ({{
|
||||
formatAmount(category.previousPeriodAmount) }})</span>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center font-semibold gap-1 text-surface-900 dark:text-surface-0">
|
||||
<div class="flex items-center gap-0">
|
||||
<span>{{ formatAmount(category.amount) }}</span>
|
||||
<span class="ml-0.5">₽</span>
|
||||
</div>
|
||||
<span class="text-xs text-surface-500 dark:text-surface-400 ml-1">({{
|
||||
formatAmount(category.previousPeriodAmount) }})</span>
|
||||
|
||||
</div>
|
||||
<div class="flex items-center text-sm gap-1 text-surface-500 dark:text-surface-400 w-10 text-right">
|
||||
<div class="flex items-center gap-0">
|
||||
<span>{{ category.percentage }}</span>
|
||||
<span class="ml-0.5">%</span>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center text-sm gap-1 text-surface-500 dark:text-surface-400 w-10 text-right">
|
||||
<div class="flex items-center gap-0">
|
||||
<span>{{ category.percentage }}</span>
|
||||
<span class="ml-0.5">%</span>
|
||||
</div>
|
||||
<span class="text-xs text-surface-500 dark:text-surface-400 ml-1">({{
|
||||
category.changeDiffPercentage > 0 ? '+' : category.changeDiffPercentage < 0 ? '-' : '' +
|
||||
Math.round(category.changeDiffPercentage) }})</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-xs text-surface-500 dark:text-surface-400 ml-1"> ({{
|
||||
category.changeDiffPercentage > 0 ? '+' : category.changeDiffPercentage < 0 ? '-' : '' +
|
||||
Math.round(category.changeDiffPercentage) }})</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isCategoriesExpanded && topCategories.length > 5" class="categories-fade"></div>
|
||||
</div>
|
||||
<Button v-if="topCategories.length > 5" :label="isCategoriesExpanded ? 'Show less' : 'Show more'"
|
||||
:icon="isCategoriesExpanded ? 'pi pi-chevron-up' : 'pi pi-chevron-down'" class="expand-button"
|
||||
@click="isCategoriesExpanded = !isCategoriesExpanded" />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
@@ -222,6 +252,11 @@ const weeklyChartOptions = computed(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Category Transactions Modal -->
|
||||
<CategoryTransactionsModal v-model:visible="isModalVisible" :categoryId="selectedCategory?.id ?? null"
|
||||
:categoryName="selectedCategory?.name ?? ''" :categoryIcon="selectedCategory?.icon ?? ''" :startDate="startDate"
|
||||
:endDate="endDate" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -242,4 +277,63 @@ const weeklyChartOptions = computed(() => {
|
||||
.dark .custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: var(--surface-700);
|
||||
}
|
||||
|
||||
.categories-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.categories-content {
|
||||
max-height: 300px;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease;
|
||||
}
|
||||
|
||||
.categories-content.expanded {
|
||||
max-height: 2000px;
|
||||
}
|
||||
|
||||
.categories-fade {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 80px;
|
||||
background: linear-gradient(to bottom,
|
||||
transparent 0%,
|
||||
rgba(255, 255, 255, 0.7) 50%,
|
||||
rgba(255, 255, 255, 0.95) 100%);
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
:global(.dark) .categories-fade {
|
||||
background: linear-gradient(to bottom,
|
||||
transparent 0%,
|
||||
rgba(24, 24, 27, 0.7) 50%,
|
||||
rgba(24, 24, 27, 0.95) 100%);
|
||||
}
|
||||
|
||||
.expand-button {
|
||||
margin-top: 1rem;
|
||||
align-self: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
||||
border: none !important;
|
||||
color: white !important;
|
||||
padding: 0.625rem 1.5rem !important;
|
||||
border-radius: 2rem !important;
|
||||
font-weight: 500 !important;
|
||||
font-size: 0.875rem !important;
|
||||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
|
||||
transition: all 0.3s ease !important;
|
||||
}
|
||||
|
||||
.expand-button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4) !important;
|
||||
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%) !important;
|
||||
}
|
||||
|
||||
.expand-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -27,7 +27,7 @@ const dashboardService = DashboardService
|
||||
|
||||
|
||||
const dashboardData = ref<DashboardData>()
|
||||
|
||||
const isAiSummaryExpanded = ref(false)
|
||||
|
||||
const dashboardTransactions = ref<Transaction[]>([]);
|
||||
const plannedTransactions = ref<Transaction[]>([]);
|
||||
@@ -48,6 +48,14 @@ const nextMonth = () => {
|
||||
fetchDashboardData();
|
||||
};
|
||||
|
||||
const currentStartDate = computed(() => {
|
||||
return currentBaseDate.value.date(10).toDate();
|
||||
});
|
||||
|
||||
const currentEndDate = computed(() => {
|
||||
return currentBaseDate.value.add(1, 'month').date(9).toDate();
|
||||
});
|
||||
|
||||
const fetchDashboardData = async () => {
|
||||
if (!spaceStore.selectedSpaceId) return;
|
||||
|
||||
@@ -158,15 +166,23 @@ const userName = computed(() => {
|
||||
<StatsCard title="Total Balance" :amount="dashboardData.balance" icon="pi pi-wallet" color="blue" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex card">
|
||||
<span class="text-xl !font-semibold pl-1">Ai summary</span>
|
||||
<span v-html="dashboardData.analyzedText" />
|
||||
<div class="flex flex-col card">
|
||||
<span class="text-xl !font-semibold pl-1 mb-2">AI Summary</span>
|
||||
<div class="ai-summary-wrapper">
|
||||
<div :class="['ai-summary-content', { 'expanded': isAiSummaryExpanded }]">
|
||||
<span v-html="dashboardData.analyzedText" />
|
||||
</div>
|
||||
<div v-if="!isAiSummaryExpanded" class="ai-summary-fade"></div>
|
||||
</div>
|
||||
<Button :label="isAiSummaryExpanded ? 'Show less' : 'Show more'"
|
||||
:icon="isAiSummaryExpanded ? 'pi pi-chevron-up' : 'pi pi-chevron-down'" class="expand-button"
|
||||
@click="isAiSummaryExpanded = !isAiSummaryExpanded" />
|
||||
</div>
|
||||
<!-- Charts & Upcoming -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-1 gap-4">
|
||||
<!-- Charts -->
|
||||
<DashboardCharts :categories="dashboardData.categories" :transactions="dashboardTransactions"
|
||||
:weeks="dashboardData.weeks" />
|
||||
:weeks="dashboardData.weeks" :startDate="currentStartDate" :endDate="currentEndDate" />
|
||||
|
||||
<!-- Upcoming Transactions -->
|
||||
<div class=" ">
|
||||
@@ -179,4 +195,67 @@ const userName = computed(() => {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.ai-summary-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ai-summary-content {
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
transition: all 0.3s ease;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.ai-summary-content.expanded {
|
||||
display: block;
|
||||
-webkit-line-clamp: unset;
|
||||
}
|
||||
|
||||
.ai-summary-fade {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3em;
|
||||
background: linear-gradient(to bottom,
|
||||
transparent 0%,
|
||||
rgba(255, 255, 255, 0.7) 50%,
|
||||
rgba(255, 255, 255, 0.95) 100%);
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
:global(.dark) .ai-summary-fade {
|
||||
background: linear-gradient(to bottom,
|
||||
transparent 0%,
|
||||
rgba(24, 24, 27, 0.7) 50%,
|
||||
rgba(24, 24, 27, 0.95) 100%);
|
||||
}
|
||||
|
||||
.expand-button {
|
||||
margin-top: 1rem;
|
||||
align-self: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
||||
border: none !important;
|
||||
color: white !important;
|
||||
padding: 0.625rem 1.5rem !important;
|
||||
border-radius: 2rem !important;
|
||||
font-weight: 500 !important;
|
||||
font-size: 0.875rem !important;
|
||||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
|
||||
transition: all 0.3s ease !important;
|
||||
}
|
||||
|
||||
.expand-button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4) !important;
|
||||
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%) !important;
|
||||
}
|
||||
|
||||
.expand-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
</style>
|
||||
@@ -9,6 +9,7 @@ export interface DashboardData {
|
||||
upcomingTransactions: Transaction[],
|
||||
recentTransactions: Transaction[],
|
||||
weeks: DashboardWeek[],
|
||||
analyzedText: string,
|
||||
}
|
||||
|
||||
export interface DashboardWeek {
|
||||
|
||||
@@ -15,12 +15,13 @@ function toDateOnly(d: Date): string {
|
||||
export interface TransactionFilters {
|
||||
type: TransactionType | null
|
||||
kind: TransactionKind | null
|
||||
categoriesIds: number[] | null
|
||||
dateFrom: string | Date | null
|
||||
dateTo: string | Date | null
|
||||
isDone: boolean | null
|
||||
offset: number | null
|
||||
limit: number | null
|
||||
sorts: Map<string, string>[]
|
||||
sorts: { sortBy: string; sortDirection: string }[]
|
||||
}
|
||||
|
||||
enum SortDirection {
|
||||
|
||||
Reference in New Issue
Block a user