analytics + categories sort in creation + auto focus on sum
This commit is contained in:
@@ -1,261 +1,62 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
import LoadingView from "@/components/LoadingView.vue";
|
import LoadingView from "@/components/LoadingView.vue";
|
||||||
import {computed, onMounted, ref} from "vue";
|
import {computed, onMounted, ref} from "vue";
|
||||||
import {getTransactionCategoriesSums} from "@/services/transactionService";
|
import {getCategories, getCategoriesSumsRequest} from "@/services/categoryService";
|
||||||
import Chart from "primevue/chart";
|
import DataTable from "primevue/datatable";
|
||||||
import MultiSelect from "primevue/multiselect";
|
import Column from "primevue/column";
|
||||||
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);
|
|
||||||
|
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const categoriesSums = ref([])
|
const categories = ref([]);
|
||||||
const dataTableCategories = 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 () => {
|
const fetchCategoriesSums = async () => {
|
||||||
loading.value = true
|
loading.value = true;
|
||||||
try {
|
await getCategoriesSumsRequest().then((data) => {
|
||||||
categoriesSums.value = await getTransactionCategoriesSums()
|
categories.value = data.data;
|
||||||
// console.log(categoriesSums.value)
|
dataTableCategories.value = prepareTableData(data.data);
|
||||||
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 = false;
|
||||||
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]
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
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 () => {
|
onMounted(async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
await Promise.all([fetchCategoriesSums(), fetchCategories()])
|
await fetchCategoriesSums();
|
||||||
loading.value = false
|
loading.value = false;
|
||||||
|
});
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
||||||
<LoadingView v-if="loading" />
|
<LoadingView v-if="loading" />
|
||||||
<div v-else class="px-4 bg-gray-100 h-full flex flex-col gap-4">
|
<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="Выберите категории"
|
<DataTable :value="dataTableCategories" responsiveLayout="scroll">
|
||||||
:maxSelectedLabels="3" class="w-full md:w-80"/>
|
<Column v-for="col in tableColumns" :key="col.field" :field="col.field" :header="col.header" />
|
||||||
|
|
||||||
<Chart v-if="selectedCategories.length > 0" type="line" :data="chartData" :options="chartOptions"
|
|
||||||
class="h-[30rem]"/>
|
|
||||||
|
|
||||||
{{dataTableCategories}}
|
|
||||||
<DataTable :value="dataTableCategories">
|
|
||||||
<!-- <Column v-for="dataTableCategories"/>-->
|
|
||||||
</DataTable>
|
</DataTable>
|
||||||
<!-- {{categories}}-->
|
|
||||||
<!-- {{// chartData}}-->
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@@ -162,16 +162,22 @@ const amountInput = ref(null);
|
|||||||
const createTransaction = async () => {
|
const createTransaction = async () => {
|
||||||
if (checkForm()) {
|
if (checkForm()) {
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
// loading.value = true;
|
||||||
if (editedTransaction.value.type.code === 'INSTANT') {
|
if (editedTransaction.value.type.code === 'INSTANT') {
|
||||||
editedTransaction.value.isDone = true;
|
editedTransaction.value.isDone = true;
|
||||||
}
|
}
|
||||||
await createTransactionRequest(editedTransaction.value);
|
await createTransactionRequest(editedTransaction.value).then
|
||||||
|
{
|
||||||
|
loading.value = false;
|
||||||
setTimeout(async () => {
|
|
||||||
amountInput.value.$el.querySelector('input').focus()
|
amountInput.value.$el.querySelector('input').focus()
|
||||||
}, 10)
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// setTimeout(async () => {
|
||||||
|
//
|
||||||
|
// amountInput.value.$el.querySelector('input').focus()
|
||||||
|
// }, 0)
|
||||||
|
|
||||||
|
|
||||||
emit('create-transaction', editedTransaction.value);
|
emit('create-transaction', editedTransaction.value);
|
||||||
@@ -197,9 +203,7 @@ const createTransaction = async () => {
|
|||||||
const transactionsUpdatedEmit = async () => {
|
const transactionsUpdatedEmit = async () => {
|
||||||
await getTransactions('INSTANT', 'EXPENSE', null, user.value.id, false, 3).then(transactionsResponse => transactions.value = transactionsResponse.data);
|
await getTransactions('INSTANT', 'EXPENSE', null, user.value.id, false, 3).then(transactionsResponse => transactions.value = transactionsResponse.data);
|
||||||
|
|
||||||
EventBus.emit('transactions-updated', {
|
EventBus.emit('transactions-updated', true)
|
||||||
id: Date.now(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновление транзакции
|
// Обновление транзакции
|
||||||
@@ -383,6 +387,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<FloatLabel variant="on" class="">
|
<FloatLabel variant="on" class="">
|
||||||
<InputNumber class=""
|
<InputNumber class=""
|
||||||
|
autofocus
|
||||||
ref="amountInput"
|
ref="amountInput"
|
||||||
:invalid="!editedTransaction.amount"
|
:invalid="!editedTransaction.amount"
|
||||||
:minFractionDigits="0"
|
:minFractionDigits="0"
|
||||||
@@ -433,8 +438,8 @@ onMounted(async () => {
|
|||||||
|
|
||||||
</FloatLabel>
|
</FloatLabel>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="flex flex-col gap-1">
|
||||||
<BudgetTransactionView v-if="!isEditing && transactions" v-for="transaction in transactions" :is-list="true"
|
<BudgetTransactionView v-if="!isEditing && transactions" v-for="transaction in transactions" :is-list="true" class="flex flexgap-4"
|
||||||
:transaction="transaction"/>
|
:transaction="transaction"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -21,13 +21,12 @@ const allLoaded = ref(false); // Флаг для отслеживания око
|
|||||||
|
|
||||||
// Функция для получения транзакций с параметрами limit и offset
|
// Функция для получения транзакций с параметрами limit и offset
|
||||||
const fetchTransactions = async (reload) => {
|
const fetchTransactions = async (reload) => {
|
||||||
console.log("here")
|
console.log(reload);
|
||||||
console.log(allLoaded.value)
|
|
||||||
// if (loading.value || allLoaded.value) return; // Останавливаем загрузку, если уже загружается или данные загружены полностью
|
// if (loading.value || allLoaded.value) return; // Останавливаем загрузку, если уже загружается или данные загружены полностью
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
try {
|
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 response = await getTransactions('INSTANT', null, null, selectedUserId.value ? selectedUserId.value : null, null, reload ? offset.value : limit, reload ? 0 : offset.value);
|
||||||
const newTransactions = response.data;
|
const newTransactions = response.data;
|
||||||
|
|
||||||
@@ -39,7 +38,7 @@ const fetchTransactions = async (reload) => {
|
|||||||
// Добавляем новые транзакции к текущему списку
|
// Добавляем новые транзакции к текущему списку
|
||||||
|
|
||||||
reload ? transactions.value = newTransactions : transactions.value.push(...newTransactions)
|
reload ? transactions.value = newTransactions : transactions.value.push(...newTransactions)
|
||||||
offset.value += limit; // Обновляем смещение для следующей загрузки
|
!reload ? offset.value += limit : offset.value
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching transactions:", error);
|
console.error("Error fetching transactions:", error);
|
||||||
}
|
}
|
||||||
@@ -110,7 +109,7 @@ const fetchUsers = async () => {
|
|||||||
const selectedTransactionType = ref(null)
|
const selectedTransactionType = ref(null)
|
||||||
const types = ref([])
|
const types = ref([])
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
EventBus.on('transactions-updated', fetchTransactions);
|
EventBus.on('transactions-updated', fetchTransactions,true);
|
||||||
await fetchTransactions(); // Первоначальная загрузка данных
|
await fetchTransactions(); // Первоначальная загрузка данных
|
||||||
await fetchUsers();
|
await fetchUsers();
|
||||||
await getTransactionTypes().then( it => types.value = it.data);
|
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) => {
|
export const deleteCategory = async (id: number) => {
|
||||||
return await apiClient.delete(`/categories/${id}`);
|
return await apiClient.delete(`/categories/${id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getCategoriesSumsRequest = async () => {
|
||||||
|
return await apiClient.get('/categories/by-month');
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user