288 lines
9.3 KiB
Vue
288 lines
9.3 KiB
Vue
<script setup lang="ts">
|
||
import LoadingView from "@/components/LoadingView.vue";
|
||
import {computed, onMounted, ref} from "vue";
|
||
import {getCategories, getCategoriesSumsRequest} from "@/services/categoryService";
|
||
import DataTable from "primevue/datatable";
|
||
import Column from "primevue/column";
|
||
import Chart from "primevue/chart";
|
||
import Listbox from "primevue/listbox";
|
||
import Select from "primevue/select";
|
||
import Accordion from "primevue/accordion";
|
||
import AccordionPanel from "primevue/accordionpanel";
|
||
import AccordionHeader from "primevue/accordionheader";
|
||
import AccordionContent from "primevue/accordioncontent"
|
||
import ChartDataLabels from 'chartjs-plugin-datalabels';
|
||
import {Chart as ChartJS} from 'chart.js/auto';
|
||
import {Colors} from 'chart.js';
|
||
import {getMonthName} from "@/utils/utils";
|
||
|
||
ChartJS.register(ChartDataLabels);
|
||
ChartJS.register(Colors);
|
||
|
||
|
||
const loading = ref(true);
|
||
const categoriesCatalog = ref([])
|
||
const categories = ref([]);
|
||
const dataTableCategories = ref([]);
|
||
const tableColumns = ref([]);
|
||
const chartData = ref(null)
|
||
const selectedCategory = ref()
|
||
const formatter = ref(new Intl.NumberFormat('ru-RU', {style: 'currency', currency: 'RUB', minimumFractionDigits: 0, maximumFractionDigits:0}))
|
||
const isChartOpen = ref(false)
|
||
|
||
const closeChart = () => {
|
||
setTimeout(() => {
|
||
isChartOpen.value = false;
|
||
}, 500)
|
||
}
|
||
|
||
|
||
const preparedChartData = computed(() => {
|
||
if (categories.value && selectedCategory.value) {
|
||
const r = Math.random() * 255
|
||
const g = Math.random() * 255
|
||
const b = Math.random() * 255
|
||
const colorLine = `rgba(${r}, ${g}, ${b},1)` // Случайный цвет для графика
|
||
const colorBackground = `rgba(${r}, ${g}, ${b},0.2)` // Случайный цвет для графика
|
||
return {
|
||
labels: categories.value[0].monthlySums.map((month) => month.date), // Используем даты как метки
|
||
datasets: [{
|
||
label: categories.value.filter((it) => selectedCategory.value.id == it._id)[0].categoryName, // Название категории
|
||
data: categories.value.filter((it) => selectedCategory.value.id == it._id)[0].monthlySums.map((month) => month.total), // Данные по total
|
||
borderColor: colorLine,
|
||
fill: true,
|
||
backgroundColor: colorBackground,
|
||
tension: 0.4
|
||
}]
|
||
|
||
}
|
||
}
|
||
})
|
||
|
||
const chartOptions = {
|
||
responsive: true,
|
||
plugins: {
|
||
legend: {
|
||
display: true,
|
||
position: "top",
|
||
},
|
||
title: {
|
||
display: true,
|
||
text: "График расходов по категориям",
|
||
},
|
||
colors: {
|
||
enabled: true,
|
||
},
|
||
datalabels: {
|
||
formatter: function (value) {
|
||
return formatter.value.format(value);
|
||
},
|
||
|
||
align: 'top',
|
||
offset: 2,
|
||
labels: {
|
||
title: {
|
||
font: {
|
||
weight: 'bold'
|
||
}
|
||
},
|
||
value: {
|
||
color: 'green'
|
||
}
|
||
}
|
||
}
|
||
},
|
||
scales: {
|
||
x: {
|
||
title: {
|
||
display: true,
|
||
text: "Месяц",
|
||
},
|
||
},
|
||
y: {
|
||
title: {
|
||
display: true,
|
||
text: "Сумма",
|
||
},
|
||
},
|
||
},
|
||
}
|
||
|
||
// Преобразование данных для таблицы
|
||
const prepareTableData = (categories) => {
|
||
// 1. Собираем все уникальные значения date из monthlySums
|
||
const allDates = [
|
||
...new Set(
|
||
categories.flatMap((category) =>
|
||
category.monthlySums.map((sumItem) => sumItem.date)
|
||
)
|
||
),
|
||
];
|
||
|
||
// 2. Сортируем даты.
|
||
// Если у вас формат "YYYY-MM-DD", лексикографическая сортировка работает корректно хронологически:
|
||
// allDates.sort((a, b) => a.localeCompare(b));
|
||
|
||
// (Если хотите гарантированно использовать объекты Date, можно так:
|
||
// allDates.sort((a, b) => new Date(a) - new Date(b));
|
||
// Но тогда поля колонки будут тоже строками вида "2024-09-01" — обычно это ок.)
|
||
|
||
// 3. Формируем колонки для DataTable:
|
||
// - Первый столбец: "category"
|
||
// - Далее по одному столбцу на каждую дату
|
||
tableColumns.value = [
|
||
{field: "category", header: "Категория"},
|
||
...allDates.map((dateStr) => ({field: dateStr, header: dateStr})),
|
||
{field: "avg", header: "Среднее"},
|
||
];
|
||
console.log(tableColumns.value[0].field);
|
||
const sums = {}
|
||
// 4. Формируем строки (для каждой категории)
|
||
const rows = categories.map((category) => {
|
||
// Начинаем со строки, где есть поле с именем категории
|
||
const row = {category: category.categoryIcon + " " + category.categoryName};
|
||
let categorySum = 0
|
||
// Для каждой даты проверяем, есть ли в monthlySums соответствующая запись
|
||
allDates.forEach((dateStr) => {
|
||
const found = category.monthlySums.find((m) => m.date === dateStr);
|
||
|
||
if (found.difference != 0) {
|
||
if (found.difference > 0) {
|
||
row[dateStr] = found ? formatter.value.format(found.total) + "<p class='text-red-500 text-sm'> (+ " + found.difference + "%)</p>" : 0;
|
||
} else {
|
||
row[dateStr] = found ? formatter.value.format(found.total) + "<p class='text-green-600 text-sm'> (" + found.difference + "%)</p>" : 0;
|
||
}
|
||
} else {
|
||
row[dateStr] = found ? formatter.value.format(found.total) : 0;
|
||
}
|
||
if (!sums[dateStr]) {
|
||
sums[dateStr] = 0
|
||
}
|
||
sums[dateStr] += found.total
|
||
categorySum += found.total
|
||
});
|
||
row["avg"] = formatter.value.format(categorySum/allDates.length);
|
||
|
||
return row;
|
||
});
|
||
|
||
|
||
let previousSum = 0;
|
||
Object.keys(sums).forEach(key => {
|
||
|
||
console.log(previousSum)
|
||
let difference = previousSum != 0 ? ((sums[key] - previousSum) / previousSum) * 100 : 0
|
||
if (sums[key] != previousSum) {
|
||
previousSum = sums[key];
|
||
}
|
||
let color = ""
|
||
if (difference > 0) {
|
||
color = "text-red-500"
|
||
} else color = "text-green-600"
|
||
sums[key] = formatter.value.format(sums[key]) + `<p class='${color}'>(` + difference.toFixed(0) + "%)</p>";
|
||
});
|
||
|
||
|
||
rows.push(sums);
|
||
return rows;
|
||
};
|
||
|
||
|
||
const fetchCategoriesSums = async () => {
|
||
loading.value = true;
|
||
await getCategoriesSumsRequest().then((data) => {
|
||
data.data.forEach((category) => {
|
||
category.monthlySums.forEach((monthlySum) => {
|
||
monthlySum.date = getMonthName(new Date(monthlySum.date).getMonth()) + " " + new Date(monthlySum.date).getFullYear()
|
||
})
|
||
})
|
||
categories.value = data.data;
|
||
console.log(categories.value);
|
||
|
||
dataTableCategories.value = prepareTableData(data.data);
|
||
|
||
});
|
||
|
||
loading.value = false;
|
||
};
|
||
|
||
const fetchCategoriesCatalog = async () => {
|
||
await getCategories("EXPENSE").then((data) => {
|
||
categoriesCatalog.value = data.data
|
||
selectedCategory.value = data.data[0]
|
||
})
|
||
}
|
||
|
||
onMounted(async () => {
|
||
await Promise.all([fetchCategoriesSums(), fetchCategoriesCatalog()])
|
||
// await fetchCategoriesSums();
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<LoadingView v-if="loading"/>
|
||
<div v-else class="p-4 bg-gray-100 h-full flex flex-col gap-4 items-center justify-items-center ">
|
||
|
||
<Accordion value="0" class=" !w-5/6 !items-center !justify-items-start" @tab-open="isChartOpen=true"
|
||
@tab-close="closeChart">
|
||
<AccordionPanel value="1">
|
||
<AccordionHeader>График</AccordionHeader>
|
||
<AccordionContent class="items-center justify-items-center ">
|
||
|
||
<!-- <Select v-model="selectedCategory" :options="categoriesCatalog" optionLabel="name"-->
|
||
<!-- placeholder="Выберите категории"-->
|
||
<!-- :maxSelectedLabels="3" class="w-full md:w-80"/>-->
|
||
<div v-if="isChartOpen" class="flex flex-row items-start justify-items-start w-full">
|
||
<Listbox v-model="selectedCategory" :options="categoriesCatalog" filter optionLabel="name"
|
||
class="!w-fit !h-5/6 md:w-56">
|
||
|
||
<template #option="slotProps">
|
||
<div>{{ slotProps.option.icon }} {{ slotProps.option.name }}</div>
|
||
</template>
|
||
|
||
</Listbox>
|
||
<Chart type="line" :data="preparedChartData" :options="chartOptions" class="!w-5/6 !h-full"/>
|
||
</div>
|
||
</AccordionContent>
|
||
</AccordionPanel>
|
||
</Accordion>
|
||
|
||
|
||
<DataTable :value="dataTableCategories" responsiveLayout="scroll" filter stripedRows class="w-5/6 items-center">
|
||
<Column
|
||
:field="tableColumns[0].field"
|
||
:header="tableColumns[0].header"
|
||
:bodyCellClass="'bold-column'"
|
||
:headerCellClass="'bold-column'"
|
||
class="font-bold"
|
||
/>
|
||
|
||
<!-- Остальные колонки -->
|
||
<Column
|
||
v-for="(col, index) in tableColumns.slice(1)"
|
||
:key="col.field"
|
||
:field="col.field"
|
||
:header="col.header"
|
||
>
|
||
<template #body="{ data }">
|
||
<span v-html="data[col.field] " class="text-center"></span>
|
||
</template>
|
||
</Column>
|
||
</DataTable>
|
||
</div>
|
||
</template>
|
||
|
||
<style>
|
||
.p-accordioncontent-content {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: start;
|
||
align-content: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.p-listbox-list-container {
|
||
max-height: 100% !important;
|
||
}
|
||
</style>
|