feat: add transaction search and filtering by date range and categories, and enhance dashboard AI summary UI with conditional rendering.
This commit is contained in:
@@ -100,8 +100,8 @@ const fetchDashboardData = async () => {
|
|||||||
dashboardData.value = await dashboardService.fetchDashboardData(spaceStore.selectedSpaceId, startDate, endDate)
|
dashboardData.value = await dashboardService.fetchDashboardData(spaceStore.selectedSpaceId, startDate, endDate)
|
||||||
dashboardTransactions.value = dashboardData.value.recentTransactions
|
dashboardTransactions.value = dashboardData.value.recentTransactions
|
||||||
plannedTransactions.value = dashboardData.value.upcomingTransactions
|
plannedTransactions.value = dashboardData.value.upcomingTransactions
|
||||||
console.log(plannedTransactions.value)
|
// console.log(plannedTransactions.value)
|
||||||
console.log(dashboardTransactions.value)
|
// console.log(dashboardTransactions.value)
|
||||||
// Fetch transactions for charts and stats (Done transactions)
|
// Fetch transactions for charts and stats (Done transactions)
|
||||||
// dashboardTransactions.value = await transactionService.getTransactions(spaceStore.selectedSpaceId, {
|
// dashboardTransactions.value = await transactionService.getTransactions(spaceStore.selectedSpaceId, {
|
||||||
// kind: "INSTANT",
|
// kind: "INSTANT",
|
||||||
@@ -123,7 +123,7 @@ const fetchDashboardData = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const setTransactionDone = async (transaction: Transaction): Promise<void> => {
|
const setTransactionDone = async (transaction: Transaction): Promise<void> => {
|
||||||
console.log(transaction)
|
// console.log(transaction)
|
||||||
const updateTransaction = {
|
const updateTransaction = {
|
||||||
type: transaction.type,
|
type: transaction.type,
|
||||||
kind: transaction.kind,
|
kind: transaction.kind,
|
||||||
@@ -134,7 +134,7 @@ const setTransactionDone = async (transaction: Transaction): Promise<void> => {
|
|||||||
isDone: !transaction.isDone,
|
isDone: !transaction.isDone,
|
||||||
date: new Date(transaction.date),
|
date: new Date(transaction.date),
|
||||||
} as UpdateTransactionDTO
|
} as UpdateTransactionDTO
|
||||||
console.log(updateTransaction)
|
// console.log(updateTransaction)
|
||||||
try {
|
try {
|
||||||
await transactionService.updateTransaction(spaceStore.selectedSpaceId as number, transaction.id, updateTransaction)
|
await transactionService.updateTransaction(spaceStore.selectedSpaceId as number, transaction.id, updateTransaction)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -202,23 +202,23 @@ const userName = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col card">
|
<div class="flex flex-col card">
|
||||||
<span class="text-xl !font-semibold pl-1 mb-2">AI Summary</span>
|
<span class="text-xl !font-semibold pl-1 mb-2">AI Summary</span>
|
||||||
<div class="ai-summary-wrapper">
|
<div v-if="dashboardData.analyzedText" class="ai-summary-wrapper">
|
||||||
<div :class="['ai-summary-content', { 'expanded': isAiSummaryExpanded }]" class="">
|
<div :class="['ai-summary-content', { 'expanded': isAiSummaryExpanded }]" class="">
|
||||||
<div class="flex flex-col gap-0">
|
<div class="flex flex-col gap-0">
|
||||||
<span class="text-lg bold">Общая оценка</span>
|
<span class="text-lg bold">Общая оценка</span>
|
||||||
<span class="!whitespace-pre-wrap" v-html="dashboardData.analyzedText.common" />
|
<span class="!whitespace-pre-wrap" v-html="dashboardData.analyzedText.common" />
|
||||||
</div>
|
</div>
|
||||||
<div class="h-4"/>
|
<div class="h-4" />
|
||||||
<div class="flex flex-col gap-0">
|
<div class="flex flex-col gap-0">
|
||||||
<span class="text-lg bold">Анализ категорий</span>
|
<span class="text-lg bold">Анализ категорий</span>
|
||||||
<span class="!whitespace-pre-wrap" v-html="dashboardData.analyzedText.categoryAnalysis" />
|
<span class="!whitespace-pre-wrap" v-html="dashboardData.analyzedText.categoryAnalysis" />
|
||||||
</div>
|
</div>
|
||||||
<div class="h-4"/>
|
<div class="h-4" />
|
||||||
<div class="flex flex-col gap-0">
|
<div class="flex flex-col gap-0">
|
||||||
<span class="text-lg bold">Ключевые инсайты</span>
|
<span class="text-lg bold">Ключевые инсайты</span>
|
||||||
<span class="!whitespace-pre-wrap" v-html="dashboardData.analyzedText.keyInsights" />
|
<span class="!whitespace-pre-wrap" v-html="dashboardData.analyzedText.keyInsights" />
|
||||||
</div>
|
</div>
|
||||||
<div class="h-4"/>
|
<div class="h-4" />
|
||||||
<div class="flex flex-col gap-0">
|
<div class="flex flex-col gap-0">
|
||||||
<span class="text-lg bold">Рекомендации</span>
|
<span class="text-lg bold">Рекомендации</span>
|
||||||
<span class="!whitespace-pre-wrap" v-html="dashboardData.analyzedText.recommendations" />
|
<span class="!whitespace-pre-wrap" v-html="dashboardData.analyzedText.recommendations" />
|
||||||
@@ -226,7 +226,13 @@ const userName = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="!isAiSummaryExpanded" class="ai-summary-fade"></div>
|
<div v-if="!isAiSummaryExpanded" class="ai-summary-fade"></div>
|
||||||
</div>
|
</div>
|
||||||
<Button :label="isAiSummaryExpanded ? 'Show less' : 'Show more'"
|
<div v-else
|
||||||
|
class="flex flex-col items-center justify-center p-8 text-center bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||||
|
<i class="pi pi-sparkles text-4xl text-blue-400 mb-3"></i>
|
||||||
|
<span class="text-lg text-gray-500 font-medium">To see AI Analysis please add some comments to your transactions
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Button v-if="dashboardData.analyzedText" :label="isAiSummaryExpanded ? 'Show less' : 'Show more'"
|
||||||
:icon="isAiSummaryExpanded ? 'pi pi-chevron-up' : 'pi pi-chevron-down'" class="expand-button"
|
:icon="isAiSummaryExpanded ? 'pi pi-chevron-up' : 'pi pi-chevron-down'" class="expand-button"
|
||||||
@click="isAiSummaryExpanded = !isAiSummaryExpanded" />
|
@click="isAiSummaryExpanded = !isAiSummaryExpanded" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useSpaceStore } from "@/stores/spaceStore";
|
import { useSpaceStore } from "@/stores/spaceStore";
|
||||||
import { onMounted, ref } from "vue";
|
import { onMounted, ref, watch } from "vue";
|
||||||
import { Checkbox, Divider } from "primevue";
|
import { Checkbox, Divider, IconField, InputIcon, InputText, Drawer, DatePicker, Button } from "primevue";
|
||||||
import { useToast } from "primevue/usetoast";
|
import { useToast } from "primevue/usetoast";
|
||||||
import { Transaction, UpdateTransactionDTO } from "@/models/transaction";
|
import { Transaction, UpdateTransactionDTO } from "@/models/transaction";
|
||||||
import { TransactionFilters, TransactionService } from "@/services/transactions-service";
|
import { TransactionFilters, TransactionService } from "@/services/transactions-service";
|
||||||
import { formatAmount, formatDate } from "@/utils/utils";
|
import { formatAmount, formatDate, toDateOnly } from "@/utils/utils";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { TransactionKind } from "@/models/enums";
|
import { TransactionKind } from "@/models/enums";
|
||||||
import { useToolbarStore } from "@/stores/toolbar-store";
|
import { useToolbarStore } from "@/stores/toolbar-store";
|
||||||
|
import { Category } from "@/models/category";
|
||||||
|
import { categoriesService } from "@/services/categories-service";
|
||||||
|
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -16,6 +19,53 @@ const spaceStore = useSpaceStore()
|
|||||||
const toolbar = useToolbarStore()
|
const toolbar = useToolbarStore()
|
||||||
const transactionService = TransactionService
|
const transactionService = TransactionService
|
||||||
|
|
||||||
|
const searchQuery = ref<string>("")
|
||||||
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
const isFilterSheetVisible = ref(false)
|
||||||
|
const filterDateFrom = ref<Date | null>(null)
|
||||||
|
const filterDateTo = ref<Date | null>(null)
|
||||||
|
const availableCategories = ref<Category[]>([])
|
||||||
|
const selectedCategoryIds = ref<number[]>([])
|
||||||
|
|
||||||
|
const fetchCategories = async () => {
|
||||||
|
if (spaceStore.selectedSpaceId) {
|
||||||
|
availableCategories.value = await categoriesService.fetchCategories(spaceStore.selectedSpaceId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleCategorySelection = (categoryId: number) => {
|
||||||
|
const index = selectedCategoryIds.value.indexOf(categoryId)
|
||||||
|
if (index === -1) {
|
||||||
|
selectedCategoryIds.value.push(categoryId)
|
||||||
|
} else {
|
||||||
|
selectedCategoryIds.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyFilters = () => {
|
||||||
|
plannedOffset.value = 0
|
||||||
|
instantOffset.value = 0
|
||||||
|
fetchData(true, true, true)
|
||||||
|
isFilterSheetVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetFilters = () => {
|
||||||
|
filterDateFrom.value = null
|
||||||
|
filterDateTo.value = null
|
||||||
|
selectedCategoryIds.value = []
|
||||||
|
applyFilters()
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(searchQuery, () => {
|
||||||
|
if (debounceTimer) clearTimeout(debounceTimer)
|
||||||
|
debounceTimer = setTimeout(() => {
|
||||||
|
plannedOffset.value = 0
|
||||||
|
instantOffset.value = 0
|
||||||
|
fetchData(true, true, true)
|
||||||
|
}, 500) // 500ms debounce
|
||||||
|
})
|
||||||
|
|
||||||
const showIsDone = ref(false)
|
const showIsDone = ref(false)
|
||||||
|
|
||||||
const setTransactionDone = async (transaction: Transaction): Promise<void> => {
|
const setTransactionDone = async (transaction: Transaction): Promise<void> => {
|
||||||
@@ -73,6 +123,10 @@ const fetchData = async (fetchPlanned: boolean = true, fetchInstant: boolean = t
|
|||||||
isDone: showIsDone.value ? undefined : false,
|
isDone: showIsDone.value ? undefined : false,
|
||||||
offset: plannedOffset.value,
|
offset: plannedOffset.value,
|
||||||
limit: plannedLimit.value,
|
limit: plannedLimit.value,
|
||||||
|
query: searchQuery.value || null,
|
||||||
|
categoriesIds: selectedCategoryIds.value.length > 0 ? selectedCategoryIds.value : null,
|
||||||
|
dateFrom: filterDateFrom.value ? toDateOnly(filterDateFrom.value) : null,
|
||||||
|
dateTo: filterDateTo.value ? toDateOnly(filterDateTo.value) : null,
|
||||||
sorts: [{ "sortBy": "date", "sortDirection": "ASC" }]
|
sorts: [{ "sortBy": "date", "sortDirection": "ASC" }]
|
||||||
} as TransactionFilters) // никаких `as TransactionFilters`, если поля опциональные
|
} as TransactionFilters) // никаких `as TransactionFilters`, если поля опциональные
|
||||||
: Promise.resolve(plannedTransactions.value)
|
: Promise.resolve(plannedTransactions.value)
|
||||||
@@ -83,6 +137,10 @@ const fetchData = async (fetchPlanned: boolean = true, fetchInstant: boolean = t
|
|||||||
kind: TransactionKind.INSTANT,
|
kind: TransactionKind.INSTANT,
|
||||||
offset: instantOffset.value,
|
offset: instantOffset.value,
|
||||||
limit: instantLimit.value,
|
limit: instantLimit.value,
|
||||||
|
query: searchQuery.value || null,
|
||||||
|
categoriesIds: selectedCategoryIds.value.length > 0 ? selectedCategoryIds.value : null,
|
||||||
|
dateFrom: filterDateFrom.value ? toDateOnly(filterDateFrom.value) : null,
|
||||||
|
dateTo: filterDateTo.value ? toDateOnly(filterDateTo.value) : null,
|
||||||
} as TransactionFilters)
|
} as TransactionFilters)
|
||||||
: Promise.resolve(instantTransactions.value)
|
: Promise.resolve(instantTransactions.value)
|
||||||
|
|
||||||
@@ -116,6 +174,7 @@ const fetchData = async (fetchPlanned: boolean = true, fetchInstant: boolean = t
|
|||||||
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
await fetchCategories()
|
||||||
await fetchData()
|
await fetchData()
|
||||||
toolbar.registerHandler('openTransactionCreation', () => {
|
toolbar.registerHandler('openTransactionCreation', () => {
|
||||||
router.push('/transactions/create')
|
router.push('/transactions/create')
|
||||||
@@ -130,12 +189,19 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
<div v-else class="flex flex-col gap-6 pb-10">
|
<div v-else class="flex flex-col gap-6 pb-10">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="card !w-full flex !flex-row gap-2">
|
||||||
|
<IconField iconPosition="left" class="!w-full !bg-white">
|
||||||
|
<InputIcon class="pi pi-search"> </InputIcon>
|
||||||
|
<InputText v-model="searchQuery" placeholder="Search" class="!w-full !bg-white !text-left" />
|
||||||
|
</IconField>
|
||||||
|
<Button icon="pi pi-filter" @click="isFilterSheetVisible = true" text rounded aria-label="Filter" />
|
||||||
|
</div>
|
||||||
<div class="flex flex-row justify-between">
|
<div class="flex flex-row justify-between">
|
||||||
<span class="text-xl !font-semibold !pl-2">Planned transactions</span>
|
<span class="text-xl !font-semibold !pl-2">Planned transactions</span>
|
||||||
<div class="flex flex-row gap-2 items-center">
|
<div class="flex flex-row gap-2 items-center">
|
||||||
<Checkbox v-model="showIsDone" binary value=" Показывать выполненные"
|
<Checkbox v-model="showIsDone" binary value=" Показывать выполненные"
|
||||||
@change="plannedOffset = 0; fetchData(true, false, true)" />
|
@change="plannedOffset = 0; fetchData(true, false, true)" />
|
||||||
<span class="!text-sm">Выполненные</span>
|
<span class="text-sm">Выполненные</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex card">
|
<div class="flex card">
|
||||||
@@ -171,6 +237,48 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
<button v-if="!plannedLastBatch" class="card w-fit " @click="fetchMorePlanned">Load more...</button>
|
<button v-if="!plannedLastBatch" class="card w-fit " @click="fetchMorePlanned">Load more...</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter Sheet -->
|
||||||
|
<Drawer v-model:visible="isFilterSheetVisible" position="bottom" style="height: auto" :modal="true"
|
||||||
|
:dismissable="true" :showCloseIcon="false">
|
||||||
|
<div class="flex flex-col gap-4 pb-6">
|
||||||
|
<div class="flex justify-between items-center px-4 pt-2">
|
||||||
|
<span class="text-xl font-bold">Filter Transactions</span>
|
||||||
|
<Button icon="pi pi-times" text rounded @click="isFilterSheetVisible = false" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col px-4 gap-2">
|
||||||
|
<span class="font-semibold text-gray-600">Period</span>
|
||||||
|
<div class="flex flex-row gap-2 w-full">
|
||||||
|
<div class="flex flex-col w-1/2">
|
||||||
|
<label class="text-sm text-gray-500 mb-1">From</label>
|
||||||
|
<DatePicker v-model="filterDateFrom" showIcon fluid :maxDate="filterDateTo || undefined" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col w-1/2">
|
||||||
|
<label class="text-sm text-gray-500 mb-1">To</label>
|
||||||
|
<DatePicker v-model="filterDateTo" showIcon fluid :minDate="filterDateFrom || undefined" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col px-4 gap-2">
|
||||||
|
<span class="font-semibold text-gray-600">Categories</span>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<div v-for="cat in availableCategories" :key="cat.id" @click="toggleCategorySelection(cat.id)"
|
||||||
|
:class="['px-3 py-2 rounded-full border cursor-pointer transition-colors flex items-center gap-2',
|
||||||
|
selectedCategoryIds.includes(cat.id) ? 'bg-blue-100 border-blue-500 text-blue-700' : 'bg-white border-gray-200 hover:bg-gray-50']">
|
||||||
|
<span>{{ cat.icon }}</span>
|
||||||
|
<span class="text-sm">{{ cat.name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-3 px-4 pt-4">
|
||||||
|
<Button label="Reset" severity="secondary" @click="resetFilters" class="w-1/3" outlined />
|
||||||
|
<Button label="Apply Filters" @click="applyFilters" class="w-2/3" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<span class="text-xl !font-semibold !pl-2">Instant transactions</span>
|
<span class="text-xl !font-semibold !pl-2">Instant transactions</span>
|
||||||
<div class="flex card">
|
<div class="flex card">
|
||||||
@@ -183,7 +291,7 @@ onMounted(async () => {
|
|||||||
<div class="flex flex-row items-center gap-2 ">
|
<div class="flex flex-row items-center gap-2 ">
|
||||||
<span v-if="instantTransactions[key].category" class="text-3xl"> {{
|
<span v-if="instantTransactions[key].category" class="text-3xl"> {{
|
||||||
instantTransactions[key].category.icon
|
instantTransactions[key].category.icon
|
||||||
}}</span>
|
}}</span>
|
||||||
<i v-else class="pi pi-question !text-3xl" />
|
<i v-else class="pi pi-question !text-3xl" />
|
||||||
<div class="flex flex-col !font-bold "> {{ instantTransactions[key].comment }}
|
<div class="flex flex-col !font-bold "> {{ instantTransactions[key].comment }}
|
||||||
<div v-if="instantTransactions[key].category" class="flex flex-row text-sm">
|
<div v-if="instantTransactions[key].category" class="flex flex-row text-sm">
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export interface DashboardData {
|
|||||||
upcomingTransactions: Transaction[],
|
upcomingTransactions: Transaction[],
|
||||||
recentTransactions: Transaction[],
|
recentTransactions: Transaction[],
|
||||||
weeks: DashboardWeek[],
|
weeks: DashboardWeek[],
|
||||||
analyzedText: AISummary,
|
analyzedText?: AISummary,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AISummary {
|
export interface AISummary {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ function toDateOnly(d: Date): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface TransactionFilters {
|
export interface TransactionFilters {
|
||||||
|
query: string | null
|
||||||
type: TransactionType | null
|
type: TransactionType | null
|
||||||
kind: TransactionKind | null
|
kind: TransactionKind | null
|
||||||
categoriesIds: number[] | null
|
categoriesIds: number[] | null
|
||||||
|
|||||||
Reference in New Issue
Block a user