analytics + categories sort in creation + auto focus on sum

This commit is contained in:
Vladimir Voronin
2025-01-12 14:15:09 +03:00
parent 60934d4335
commit 086015001e
4 changed files with 71 additions and 261 deletions

View File

@@ -1,261 +1,62 @@
<script setup lang="ts">
import LoadingView from "@/components/LoadingView.vue";
import {computed, onMounted, ref} from "vue";
import {getTransactionCategoriesSums} from "@/services/transactionService";
import Chart from "primevue/chart";
import MultiSelect from "primevue/multiselect";
import {format} from "date-fns";
import {generateRandomColors} from "@/utils/utils";
import {Category} from "@/models/Category";
import {getCategories} from "@/services/categoryService";
import {Chart as ChartJS} from "chart.js/auto";
import ChartDataLabels from "chartjs-plugin-datalabels";
ChartJS.register(ChartDataLabels);
import {getCategories, getCategoriesSumsRequest} from "@/services/categoryService";
import DataTable from "primevue/datatable";
import Column from "primevue/column";
const loading = ref(false);
const categoriesSums = ref([])
const dataTableCategories = ref([])
const categories = ref([]);
const dataTableCategories = ref([]);
const tableColumns = ref([]);
// Преобразование данных для таблицы
const prepareTableData = (categories) => {
const allMonths = [
...new Set(
categories.flatMap((category) =>
category.monthlyData.map((monthData) => monthData.month)
)
),
];
tableColumns.value = [
{ field: "category", header: "Категория" },
...allMonths.map((month) => ({ field: month, header: month })),
]; // Устанавливаем столбцы для DataTable
return categories.map((category) => {
const row = { category: category.categoryName };
allMonths.forEach((month) => {
const data = category.monthlyData.find((m) => m.month === month);
row[month] = data ? data.totalAmount : 0;
});
return row;
});
};
const fetchCategoriesSums = async () => {
loading.value = true
try {
categoriesSums.value = await getTransactionCategoriesSums()
// console.log(categoriesSums.value)
for (let category of categoriesSums.value) {
// console.log(category)
// [{
// "category": category[0],
// "category"+[category[1]]: category[2]
// }]
// console.log('test')
// console.log('dataTableCategories '+ dataTableCategories.value)
let categoryInList = dataTableCategories.value.find((listCategory: Category) => {
// console.log(listCategory['category'].id)
// console.log(category[0].id)
return listCategory['category'].id === category[0].id
})
console.log('cat in list ' + categoryInList)
if (categoryInList) {
console.log('cat[1] '+ category[1])
console.log('cat[2] '+ category[2])
categoryInList[category[1]] = category[2]
// console.log(categoryInList)
} else {
dataTableCategories.value.push({'category': category[0]})
dataTableCategories.value.filter((listCategory: Category) => {
return listCategory['category'].id === category.id
})[category[1]] = category[2]
}
// console.log(categoryInList)
}
// console.log(dataTableCategories.value)
// console.log(categoriesSums.value)
} catch (error) {
console.error('Error fetching categories sums:', error);
}
loading.value = false
}
const categories = ref<Category[]>([])
const selectedCategories = ref([])
const fetchCategories = async () => {
loading.value = true
try {
const response = await getCategories('EXPENSE');
categories.value = response.data
console.log(categories.value.filter(i => i.id == 30))
selectedCategories.value.push(categories.value.filter(i => i.id == 30)[0])
} catch (error) {
console.error('Error fetching categories:', error);
}
loading.value = false
}
const chartData = computed(() => {
return {
labels: chartLabels.value[0],
datasets: chartLabels.value[1]
};
})
const chartOptions = computed(() => {
return {
maintainAspectRatio: false,
aspectRatio: 0.6,
plugins: {
legend: {
labels: {
color: 'rgba(0, 0, 0, 1)',
font: {
weight: 'bold',
}
}
}
},
scales: {
x: {
ticks: {
color: 'rgba(0, 0, 0, 0.5)',
},
grid: {
color: 'rgba(255, 0, 0, 0.5)',
}
},
y: {
ticks: {
color: 'rgba(0, 0, 0, 0.5)',
},
grid: {
color: 'rgba(0, 0, 0, 0.5)',
}
}
}
};
})
const chartLabels = computed(() => {
let dates = new Array<Date>()
categoriesSums.value.filter(i => i[0].id == 30).forEach((item) => {
dates.push(format(new Date(item[1]), 'MM.yy'))
})
let datasets = []
categoriesSums.value.forEach((item) => {
// Проверка, есть ли категория в массиве выбранных категорий `selectedCategories`
const isSelected = selectedCategories.value.some((category) => category.id == item[0].id);
if (!isSelected) {
return; // Пропускаем категории, которых нет в `selectedCategories`
}
// Проверка, есть ли уже категория с таким названием в `datasets`
const existingDataset = datasets.find((v) => v.label === item[0].name);
let color = generateRandomColors();
if (!existingDataset) {
// Создаем новый объект `dataset` для новой категории
let dataset = {
label: item[0].name,
data: categoriesSums.value
.filter((i) => i[0].id === item[0].id)
.map((value) => value[2]), // Собираем массив значений для категории
fill: true,
borderColor: `rgba(${color[0]}, ${color[1]}, ${color[2]}, 1)`,
backgroundColor: `rgba(${color[0]}, ${color[1]}, ${color[2]}, 0.2)`,
tension: 0.4,
};
// Добавляем созданный объект `dataset` в массив `datasets`
datasets.push(dataset);
}
loading.value = true;
await getCategoriesSumsRequest().then((data) => {
categories.value = data.data;
dataTableCategories.value = prepareTableData(data.data);
});
return [dates, datasets];
})
// const categories = computed(() => {
// let categoriesIds = new Array<number>()
// categoriesSums.value.forEach((i) => {
// categoriesIds.push(i[0].id)
// })
// return categoriesIds
// })
const setChartData = () => {
console.log(chartLabels.value[1])
return {
labels: chartLabels.value[0],
datasets: chartLabels.value[1]
loading.value = false;
};
}
const setChartOptions = () => {
// const documentStyle = getComputedStyle(document.documentElement);
// const textColor = documentStyle.getPropertyValue('--p-text-color');
// const textColorSecondary = documentStyle.getPropertyValue('--p-text-muted-color');
// const surfaceBorder = documentStyle.getPropertyValue('--p-content-border-color');
return {
maintainAspectRatio: false,
aspectRatio: 0.6,
plugins: {
legend: {
labels: {
color: 'rgba(0, 0, 0, 1)',
font: {
weight: 'bold'
}
}
},
datalabels: {
color: 'rgba(0, 0, 0, 1)', // Цвет текста
anchor: 'end', // Привязка метки
align: 'top', // Выравнивание над точкой
offset: 8, // Отступ от точки
labels: {
font: {
weight: 'bold'
}
}
}
},
scales: {
x: {
ticks: {
color: 'rgba(0, 0, 0, 0.5)',
},
grid: {
color: 'rgba(255, 0, 0, 0.5)',
}
},
y: {
ticks: {
color: 'rgba(0, 0, 0, 0.5)',
},
grid: {
color: 'rgba(0, 0, 0, 0.5)',
}
}
}
};
}
onMounted(async () => {
loading.value = true;
await Promise.all([fetchCategoriesSums(), fetchCategories()])
loading.value = false
})
await fetchCategoriesSums();
loading.value = false;
});
</script>
<template>
<LoadingView v-if="loading" />
<div v-else class="px-4 bg-gray-100 h-full flex flex-col gap-4">
<MultiSelect v-model="selectedCategories" :options="categories" optionLabel="name" filter
placeholder="Выберите категории"
:maxSelectedLabels="3" class="w-full md:w-80"/>
<Chart v-if="selectedCategories.length > 0" type="line" :data="chartData" :options="chartOptions"
class="h-[30rem]"/>
{{dataTableCategories}}
<DataTable :value="dataTableCategories">
<!-- <Column v-for="dataTableCategories"/>-->
<!-- Таблица с преобразованными данными -->
<DataTable :value="dataTableCategories" responsiveLayout="scroll">
<Column v-for="col in tableColumns" :key="col.field" :field="col.field" :header="col.header" />
</DataTable>
<!-- {{categories}}-->
<!-- {{// chartData}}-->
</div>
</template>
<style scoped>
</style>

View File

@@ -162,16 +162,22 @@ const amountInput = ref(null);
const createTransaction = async () => {
if (checkForm()) {
try {
loading.value = true;
// loading.value = true;
if (editedTransaction.value.type.code === 'INSTANT') {
editedTransaction.value.isDone = true;
}
await createTransactionRequest(editedTransaction.value);
setTimeout(async () => {
await createTransactionRequest(editedTransaction.value).then
{
loading.value = false;
amountInput.value.$el.querySelector('input').focus()
}, 10)
}
// setTimeout(async () => {
//
// amountInput.value.$el.querySelector('input').focus()
// }, 0)
emit('create-transaction', editedTransaction.value);
@@ -197,9 +203,7 @@ const createTransaction = async () => {
const transactionsUpdatedEmit = async () => {
await getTransactions('INSTANT', 'EXPENSE', null, user.value.id, false, 3).then(transactionsResponse => transactions.value = transactionsResponse.data);
EventBus.emit('transactions-updated', {
id: Date.now(),
});
EventBus.emit('transactions-updated', true)
}
// Обновление транзакции
@@ -383,6 +387,7 @@ onMounted(async () => {
<FloatLabel variant="on" class="">
<InputNumber class=""
autofocus
ref="amountInput"
:invalid="!editedTransaction.amount"
:minFractionDigits="0"
@@ -433,8 +438,8 @@ onMounted(async () => {
</FloatLabel>
</div>
<div>
<BudgetTransactionView v-if="!isEditing && transactions" v-for="transaction in transactions" :is-list="true"
<div class="flex flex-col gap-1">
<BudgetTransactionView v-if="!isEditing && transactions" v-for="transaction in transactions" :is-list="true" class="flex flexgap-4"
:transaction="transaction"/>
</div>

View File

@@ -21,13 +21,12 @@ const allLoaded = ref(false); // Флаг для отслеживания око
// Функция для получения транзакций с параметрами limit и offset
const fetchTransactions = async (reload) => {
console.log("here")
console.log(allLoaded.value)
console.log(reload);
// if (loading.value || allLoaded.value) return; // Останавливаем загрузку, если уже загружается или данные загружены полностью
loading.value = true;
try {
console.log(reload);
const response = await getTransactions('INSTANT', null, null, selectedUserId.value ? selectedUserId.value : null, null, reload ? offset.value : limit, reload ? 0 : offset.value);
const newTransactions = response.data;
@@ -39,7 +38,7 @@ const fetchTransactions = async (reload) => {
// Добавляем новые транзакции к текущему списку
reload ? transactions.value = newTransactions : transactions.value.push(...newTransactions)
offset.value += limit; // Обновляем смещение для следующей загрузки
!reload ? offset.value += limit : offset.value
} catch (error) {
console.error("Error fetching transactions:", error);
}
@@ -110,7 +109,7 @@ const fetchUsers = async () => {
const selectedTransactionType = ref(null)
const types = ref([])
onMounted(async () => {
EventBus.on('transactions-updated', fetchTransactions);
EventBus.on('transactions-updated', fetchTransactions,true);
await fetchTransactions(); // Первоначальная загрузка данных
await fetchUsers();
await getTransactionTypes().then( it => types.value = it.data);

View File

@@ -23,3 +23,8 @@ export const updateCategory = async (id: number, category: any) => {
export const deleteCategory = async (id: number) => {
return await apiClient.delete(`/categories/${id}`);
};
export const getCategoriesSumsRequest = async () => {
return await apiClient.get('/categories/by-month');
}