analytics + categories sort in creation + auto focus on sum
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user