fixessome
This commit is contained in:
15
package-lock.json
generated
15
package-lock.json
generated
@@ -18,6 +18,7 @@
|
|||||||
"pinia": "^2.2.6",
|
"pinia": "^2.2.6",
|
||||||
"platform": "^1.3.6",
|
"platform": "^1.3.6",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
|
"primelocale": "^1.0.4",
|
||||||
"primevue": "^4.1.0",
|
"primevue": "^4.1.0",
|
||||||
"tailwindcss-primeui": "^0.3.4",
|
"tailwindcss-primeui": "^0.3.4",
|
||||||
"vue": "^3.5.11",
|
"vue": "^3.5.11",
|
||||||
@@ -7155,6 +7156,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz",
|
||||||
"integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw=="
|
"integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/primelocale": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/primelocale/-/primelocale-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-98hx5Nwq7CusZnb2ToOLBsamroihcvCPwxK8CUOXMCwoL7nn5jwL7QdEQWzFuTkE3UAAgBWYQwi1eITtzpTulw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0",
|
||||||
|
"npm": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/primevue": {
|
"node_modules/primevue": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/primevue/-/primevue-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/primevue/-/primevue-4.1.0.tgz",
|
||||||
@@ -14656,6 +14666,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz",
|
||||||
"integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw=="
|
"integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw=="
|
||||||
},
|
},
|
||||||
|
"primelocale": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/primelocale/-/primelocale-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-98hx5Nwq7CusZnb2ToOLBsamroihcvCPwxK8CUOXMCwoL7nn5jwL7QdEQWzFuTkE3UAAgBWYQwi1eITtzpTulw=="
|
||||||
|
},
|
||||||
"primevue": {
|
"primevue": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/primevue/-/primevue-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/primevue/-/primevue-4.1.0.tgz",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"pinia": "^2.2.6",
|
"pinia": "^2.2.6",
|
||||||
"platform": "^1.3.6",
|
"platform": "^1.3.6",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
|
"primelocale": "^1.0.4",
|
||||||
"primevue": "^4.1.0",
|
"primevue": "^4.1.0",
|
||||||
"tailwindcss-primeui": "^0.3.4",
|
"tailwindcss-primeui": "^0.3.4",
|
||||||
"vue": "^3.5.11",
|
"vue": "^3.5.11",
|
||||||
|
|||||||
14
src/App.vue
14
src/App.vue
@@ -9,7 +9,7 @@
|
|||||||
<div class="flex flex-col flex-grow gap-4 ">
|
<div class="flex flex-col flex-grow gap-4 ">
|
||||||
<!-- {{ tg_id }}-->
|
<!-- {{ tg_id }}-->
|
||||||
<Button label="Sub" :class="checkNotif ? 'flex' : '!hidden'" @click="checkSubscribe"/>
|
<Button label="Sub" :class="checkNotif ? 'flex' : '!hidden'" @click="checkSubscribe"/>
|
||||||
<router-view class="pt-2"/>
|
<router-view />
|
||||||
<div class="bg-gray-100 h-12 block lg:hidden"></div>
|
<div class="bg-gray-100 h-12 block lg:hidden"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ import {subscribeUserToPush} from "@/services/pushManager";
|
|||||||
import apiClient from '@/services/axiosSetup';
|
import apiClient from '@/services/axiosSetup';
|
||||||
import {useUserStore} from "@/stores/userStore";
|
import {useUserStore} from "@/stores/userStore";
|
||||||
import {useDrawerStore} from '@/stores/drawerStore'
|
import {useDrawerStore} from '@/stores/drawerStore'
|
||||||
import TransactionForm from "@/components/TransactionForm.vue";
|
import TransactionForm from "@/components/transactions/TransactionForm.vue";
|
||||||
|
|
||||||
|
|
||||||
const drawerStore = useDrawerStore();
|
const drawerStore = useDrawerStore();
|
||||||
@@ -79,17 +79,17 @@ const sendSubscribe = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('vyzyvaem app')
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const user = computed(() => userStore.user);
|
const user = computed(() => userStore.user);
|
||||||
console.log('vyzvali app')
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
console.log("Загружаем данные при монтировании...");
|
|
||||||
if (!userStore.user) {
|
if (!userStore.user) {
|
||||||
console.log('vyzyvaem app2')
|
|
||||||
await userStore.fetchUserProfile();
|
await userStore.fetchUserProfile();
|
||||||
console.log('vyzvali app2')
|
|
||||||
}
|
}
|
||||||
await checkSubscribe();
|
await checkSubscribe();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<Drawer :visible="visible" :header="isEditing ? 'Изменить транзакцию ' : 'Создать транзакцию'" :showCloseIcon="false"
|
<Drawer :visible="visible" :header="isEditing ? 'Изменить транзакцию ' : 'Создать транзакцию'" :showCloseIcon="true"
|
||||||
position="right" @hide="closeDrawer"
|
position="right" @hide="closeDrawer" @update:visible="closeDrawer"
|
||||||
class="!w-128 hidden lg:block ">
|
class="!w-128 hidden lg:block ">
|
||||||
<slot />
|
<slot />
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="hello">
|
|
||||||
<h1>{{ msg }}</h1>
|
|
||||||
<p>
|
|
||||||
For a guide and recipes on how to configure / customize this project,<br>
|
|
||||||
check out the
|
|
||||||
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
|
|
||||||
</p>
|
|
||||||
<h3>Installed CLI Plugins</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript" target="_blank" rel="noopener">typescript</a></li>
|
|
||||||
</ul>
|
|
||||||
<h3>Essential Links</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
|
|
||||||
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
|
|
||||||
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
|
|
||||||
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
|
|
||||||
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
|
|
||||||
</ul>
|
|
||||||
<h3>Ecosystem</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
|
|
||||||
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
|
|
||||||
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
|
|
||||||
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
|
|
||||||
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Options, Vue } from 'vue-class-component';
|
|
||||||
|
|
||||||
@Options({
|
|
||||||
props: {
|
|
||||||
msg: String
|
|
||||||
}
|
|
||||||
})
|
|
||||||
export default class HelloWorld extends Vue {
|
|
||||||
msg!: string
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
|
||||||
<style scoped>
|
|
||||||
h3 {
|
|
||||||
margin: 40px 0 0;
|
|
||||||
}
|
|
||||||
ul {
|
|
||||||
list-style-type: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
li {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0 10px;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: #42b983;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
<template #end>
|
<template #end>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
{{ user.firstName }}
|
{{ user.firstName }}
|
||||||
<Button @click="drawerStore.visible = true" label="Create"/>
|
<Button @click="drawerStore.visible = true" label="Создать"/>
|
||||||
|
|
||||||
<!-- <InputText placeholder="Search" type="text" class="w-32 sm:w-auto" />-->
|
<!-- <InputText placeholder="Search" type="text" class="w-32 sm:w-auto" />-->
|
||||||
<!-- <Avatar image="https://primefaces.org/cdn/primevue/images/avatar/amyelsner.png" shape="circle" />-->
|
<!-- <Avatar image="https://primefaces.org/cdn/primevue/images/avatar/amyelsner.png" shape="circle" />-->
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import WelcomeItem from './WelcomeItem.vue'
|
|
||||||
import DocumentationIcon from './icons/IconDocumentation.vue'
|
|
||||||
import ToolingIcon from './icons/IconTooling.vue'
|
|
||||||
import EcosystemIcon from './icons/IconEcosystem.vue'
|
|
||||||
import CommunityIcon from './icons/IconCommunity.vue'
|
|
||||||
import SupportIcon from './icons/IconSupport.vue'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<DocumentationIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Documentation</template>
|
|
||||||
|
|
||||||
Vue’s
|
|
||||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
|
|
||||||
provides you with all information you need to get started.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<ToolingIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Tooling</template>
|
|
||||||
|
|
||||||
This project is served and bundled with
|
|
||||||
<a href="https://vite.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
|
|
||||||
recommended IDE setup is
|
|
||||||
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a> +
|
|
||||||
<a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
|
|
||||||
you need to test your components and web pages, check out
|
|
||||||
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a> and
|
|
||||||
<a href="https://on.cypress.io/component" target="_blank" rel="noopener"
|
|
||||||
>Cypress Component Testing</a
|
|
||||||
>.
|
|
||||||
|
|
||||||
<br />
|
|
||||||
|
|
||||||
More instructions are available in <code>README.md</code>.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<EcosystemIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Ecosystem</template>
|
|
||||||
|
|
||||||
Get official tools and libraries for your project:
|
|
||||||
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
|
|
||||||
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
|
|
||||||
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
|
|
||||||
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
|
|
||||||
you need more resources, we suggest paying
|
|
||||||
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
|
|
||||||
a visit.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<CommunityIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Community</template>
|
|
||||||
|
|
||||||
Got stuck? Ask your question on
|
|
||||||
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official
|
|
||||||
Discord server, or
|
|
||||||
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
|
|
||||||
>StackOverflow</a
|
|
||||||
>. You should also subscribe to
|
|
||||||
<a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and follow
|
|
||||||
the official
|
|
||||||
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
|
|
||||||
twitter account for latest news in the Vue world.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<SupportIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Support Vue</template>
|
|
||||||
|
|
||||||
As an independent project, Vue relies on community backing for its sustainability. You can help
|
|
||||||
us by
|
|
||||||
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
|
|
||||||
</WelcomeItem>
|
|
||||||
</template>
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="item">
|
|
||||||
<i>
|
|
||||||
<slot name="icon"></slot>
|
|
||||||
</i>
|
|
||||||
<div class="details">
|
|
||||||
<h3>
|
|
||||||
<slot name="heading"></slot>
|
|
||||||
</h3>
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.item {
|
|
||||||
margin-top: 2rem;
|
|
||||||
display: flex;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.details {
|
|
||||||
flex: 1;
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
place-content: center;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 0.4rem;
|
|
||||||
color: var(--color-heading);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.item {
|
|
||||||
margin-top: 0;
|
|
||||||
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
top: calc(50% - 25px);
|
|
||||||
left: -26px;
|
|
||||||
position: absolute;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
background: var(--color-background);
|
|
||||||
border-radius: 8px;
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:before {
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
bottom: calc(50% + 25px);
|
|
||||||
height: calc(50% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:after {
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: calc(50% + 25px);
|
|
||||||
height: calc(50% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:first-of-type:before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:last-of-type:after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
225
src/components/analytics/AnalyticsView.vue
Normal file
225
src/components/analytics/AnalyticsView.vue
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
<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);
|
||||||
|
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const categoriesSums = ref([])
|
||||||
|
|
||||||
|
const fetchCategoriesSums = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
categoriesSums.value = await getTransactionCategoriesSums()
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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 () => {
|
||||||
|
loading.value = true;
|
||||||
|
await Promise.all([fetchCategoriesSums(), fetchCategories()])
|
||||||
|
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]"/>
|
||||||
|
|
||||||
|
<!-- {{categories}}-->
|
||||||
|
<!-- {{// chartData}}-->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -16,7 +16,7 @@ const props = defineProps({
|
|||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const emits = defineEmits(['close-modal'])
|
const emits = defineEmits(['budget-created','close-modal'])
|
||||||
const createRecurrentPayments = ref<Boolean>(true)
|
const createRecurrentPayments = ref<Boolean>(true)
|
||||||
|
|
||||||
const name = ref('')
|
const name = ref('')
|
||||||
@@ -29,7 +29,7 @@ const create = async () => {
|
|||||||
console.log(budget.value)
|
console.log(budget.value)
|
||||||
try {
|
try {
|
||||||
await createBudget(budget.value, createRecurrentPayments.value)
|
await createBudget(budget.value, createRecurrentPayments.value)
|
||||||
emits("close-modal");
|
emits("budget-created");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
@@ -65,7 +65,7 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Dialog :visible="opened" modal header="Создать новый бюджет" :style="{ width: '25rem' }">
|
<Dialog :visible="opened" modal header="Создать новый бюджет" :style="{ width: '25rem' }" @hide="cancel" @update:visible="cancel">
|
||||||
<div class="flex flex-col gap-4 mt-1">
|
<div class="flex flex-col gap-4 mt-1">
|
||||||
<FloatLabel variant="on" class="w-full">
|
<FloatLabel variant="on" class="w-full">
|
||||||
<label for="name">Название</label>
|
<label for="name">Название</label>
|
||||||
@@ -87,7 +87,7 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row gap-2 justify-end items-center">
|
<div class="flex flex-row gap-2 justify-end items-center">
|
||||||
<Button label="Создать" severity="success" icon="pi pi-save" @click="create"/>
|
<Button label="Создать" severity="success" icon="pi pi-save" @click="create"/>
|
||||||
<Button label="Отмена" severity="secondary" icon="pi pi-times-circle"/>
|
<Button label="Отмена" severity="secondary" icon="pi pi-times-circle" @click="cancel"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div class="flex flex-row gap-4 items-center">
|
<div class="flex flex-row gap-4 items-center">
|
||||||
<h2 class="text-4xl font-bold">Бюджеты</h2>
|
<h2 class="text-4xl font-bold">Бюджеты</h2>
|
||||||
<Button label="+ Создать" @click="creationOpened=true" size="small"/>
|
<Button label="+ Создать" @click="creationOpened=true" size="small"/>
|
||||||
<BudgetCreationView :opened="creationOpened" @close-modal="creationOpened = false; creationSuccessShow() "/>
|
<BudgetCreationView :opened="creationOpened" @budget-created="creationSuccessShow()" @close-modal="creationOpened=false" />
|
||||||
<StatusView :show="creationSuccessModal" :is-error="false" :message="'Бюджет создан!'"/>
|
<StatusView :show="creationSuccessModal" :is-error="false" :message="'Бюджет создан!'"/>
|
||||||
</div>
|
</div>
|
||||||
<!-- Плитка с бюджетами -->
|
<!-- Плитка с бюджетами -->
|
||||||
@@ -22,44 +22,43 @@
|
|||||||
<div class="text-sm text-gray-600 mb-4">
|
<div class="text-sm text-gray-600 mb-4">
|
||||||
{{ formatDate(budget.dateFrom) }} - {{ formatDate(budget.dateTo) }}
|
{{ formatDate(budget.dateFrom) }} - {{ formatDate(budget.dateTo) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<!-- <div class="mb-4">-->
|
||||||
<!-- <div class="text-sm">Total Income: <span class="font-bold">{{ formatAmount(budgettotalIncomes) }} ₽</span></div>-->
|
<!-- <div class="text-sm">Total Income: <span class="font-bold">{{ formatAmount(budgettotalIncomes) }} ₽</span></div>-->
|
||||||
<!-- <div class="text-sm">Total Expenses: <span class="font-bold">{{ formatAmount(budget.totalExpenses) }} ₽</span></div>-->
|
<!-- <div class="text-sm">Total Expenses: <span class="font-bold">{{ formatAmount(budget.totalExpenses) }} ₽</span></div>-->
|
||||||
<!-- <div class="text-sm">Planned Expenses: <span class="font-bold">{{ formatAmount(budget.totalExpenses) }} ₽</span></div>-->
|
<!-- <div class="text-sm">Planned Expenses: <span class="font-bold">{{ formatAmount(budget.totalExpenses) }} ₽</span></div>-->
|
||||||
<div class="text-sm flex items-center">
|
<!-- <div class="text-sm flex items-center">-->
|
||||||
Unplanned Expenses:
|
<!-- Unplanned Expenses:-->
|
||||||
<!-- <span class="ml-2 font-bold">{{ formatAmount(budget.totalIncomes - budget.totalExpenses) }} ₽</span>-->
|
<!-- <span class="ml-2 font-bold">{{ formatAmount(budget.totalIncomes - budget.totalExpenses) }} ₽</span>-->
|
||||||
<!-- Прогресс бар -->
|
<!-- Прогресс бар -->
|
||||||
<!-- <ProgressBar :value="budget.unplannedProgress" class="ml-4 w-full"/>-->
|
<!-- <ProgressBar :value="budget.unplannedProgress" class="ml-4 w-full"/>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Прошедшие бюджеты (забеленные) -->
|
<!-- Прошедшие бюджеты (забеленные) -->
|
||||||
<div v-for="budget in pastBudgets" :key="budget.id" class="p-4 shadow-lg rounded-lg bg-gray-100 opacity-60">
|
<!-- <div v-for="budget in pastBudgets" :key="budget.id" class="p-4 shadow-lg rounded-lg bg-gray-100 opacity-60">-->
|
||||||
<div class="text-xl font-bold mb-2">{{ budget.month }}</div>
|
<!-- <div class="text-xl font-bold mb-2">{{ budget.month }}</div>-->
|
||||||
<div class="text-sm text-gray-600 mb-4">
|
<!-- <div class="text-sm text-gray-600 mb-4">-->
|
||||||
{{ budget.startDate }} - {{ budget.endDate }}
|
<!-- {{ budget.startDate }} - {{ budget.endDate }}-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
<div class="mb-4">
|
<!-- <div class="mb-4">-->
|
||||||
<div class="text-sm">Total Income: <span class="font-bold">{{ budget.totalIncome }}</span></div>
|
<!-- <div class="text-sm">Total Income: <span class="font-bold">{{ budget.totalIncome }}</span></div>-->
|
||||||
<div class="text-sm">Total Expenses: <span class="font-bold">{{ budget.totalExpenses }}</span></div>
|
<!-- <div class="text-sm">Total Expenses: <span class="font-bold">{{ budget.totalExpenses }}</span></div>-->
|
||||||
<div class="text-sm">Planned Expenses: <span class="font-bold">{{ budget.plannedExpenses }}</span></div>
|
<!-- <div class="text-sm">Planned Expenses: <span class="font-bold">{{ budget.plannedExpenses }}</span></div>-->
|
||||||
<div class="text-sm flex items-center">
|
<!-- <div class="text-sm flex items-center">-->
|
||||||
Unplanned Expenses:
|
<!-- Unplanned Expenses:-->
|
||||||
<span class="ml-2 font-bold">{{ budget.remainingForUnplanned }}</span>
|
<!-- <span class="ml-2 font-bold">{{ budget.remainingForUnplanned }}</span>-->
|
||||||
<!-- Прогресс бар -->
|
<!-- <!– Прогресс бар –>-->
|
||||||
<ProgressBar :value="budget.unplannedProgress" class="ml-4 w-full"/>
|
<!-- <ProgressBar :value="budget.unplannedProgress" class="ml-4 w-full"/>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {onMounted, ref} from 'vue';
|
import {onMounted, ref} from 'vue';
|
||||||
import ProgressBar from 'primevue/progressbar';
|
|
||||||
import {BudgetInfo} from "@/models/Budget";
|
import {BudgetInfo} from "@/models/Budget";
|
||||||
import {getBudgetInfos} from "@/services/budgetsService";
|
import {getBudgetInfos} from "@/services/budgetsService";
|
||||||
import {formatDate} from "@/utils/utils";
|
import {formatDate} from "@/utils/utils";
|
||||||
@@ -74,6 +73,7 @@ const budgetInfos = ref<BudgetInfo[]>([])
|
|||||||
const creationOpened = ref(false)
|
const creationOpened = ref(false)
|
||||||
const creationSuccessModal = ref(false)
|
const creationSuccessModal = ref(false)
|
||||||
const creationSuccessShow = async () => {
|
const creationSuccessShow = async () => {
|
||||||
|
creationOpened.value = false
|
||||||
budgetInfos.value = await getBudgetInfos()
|
budgetInfos.value = await getBudgetInfos()
|
||||||
creationSuccessModal.value = true
|
creationSuccessModal.value = true
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {Category, CategoryType} from "@/models/Category";
|
|||||||
import {getCategories, getCategoryTypes} from "@/services/categoryService";
|
import {getCategories, getCategoryTypes} from "@/services/categoryService";
|
||||||
import {setTransactionDoneRequest} from "@/services/transactionService";
|
import {setTransactionDoneRequest} from "@/services/transactionService";
|
||||||
import {formatAmount, formatDate} from "@/utils/utils";
|
import {formatAmount, formatDate} from "@/utils/utils";
|
||||||
import TransactionForm from "@/components/TransactionForm.vue";
|
import TransactionForm from "@/components/transactions/TransactionForm.vue";
|
||||||
|
|
||||||
|
|
||||||
const props = defineProps(
|
const props = defineProps(
|
||||||
|
|||||||
@@ -121,6 +121,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<button class="grid grid-cols-2 justify-between gap-5 items-end w-full"
|
||||||
|
@click="detailedShowed = !detailedShowed">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<h4 class="text-sm lg:text-base">Факт. поступления ✅</h4>
|
||||||
|
<div class="font-light bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center">
|
||||||
|
{{ formatAmount(totalInstantIncomes) }} ₽
|
||||||
|
</div>
|
||||||
|
<!-- <p>Total Incomes</p>-->
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center ">
|
||||||
|
<span class="text-sm lg:text-base">Факт. траты 📛</span>
|
||||||
|
<div class="font-light bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center">
|
||||||
|
{{ formatAmount(totalInstantExpenses)}} ₽
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</button>
|
||||||
<div class="grid grid-cols-2 !gap-1 mt-4" :class="detailedShowed ? 'block' : 'hidden'">
|
<div class="grid grid-cols-2 !gap-1 mt-4" :class="detailedShowed ? 'block' : 'hidden'">
|
||||||
<div v-for="categorySum in transactionCategoriesSums" class="flex flex-col items-center font-bold ">
|
<div v-for="categorySum in transactionCategoriesSums" class="flex flex-col items-center font-bold ">
|
||||||
<p class="font-light ">{{ categorySum.category.icon }} {{ categorySum.category.name }}</p>
|
<p class="font-light ">{{ categorySum.category.icon }} {{ categorySum.category.name }}</p>
|
||||||
@@ -139,20 +159,19 @@
|
|||||||
<div class="flex flex-row gap-4">
|
<div class="flex flex-row gap-4">
|
||||||
<h3 class="text-2xl font-bold">Транзакции</h3>
|
<h3 class="text-2xl font-bold">Транзакции</h3>
|
||||||
<button @click="openDrawer('INSTANT', 'EXPENSE')">
|
<button @click="openDrawer('INSTANT', 'EXPENSE')">
|
||||||
<!-- <i class="pi pi-plus-circle text-green-500" style="font-size: 1rem;"/>-->
|
|
||||||
<span class="font-light text-sm">+ Добавить</span>
|
<span class="font-light text-sm">+ Добавить</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class=" flex gap-2 overflow-x-auto ">
|
<div class=" flex gap-2 overflow-x-auto ">
|
||||||
|
|
||||||
<button v-for="categorySum in transactionCategoriesSums"
|
<button v-for="categorySum in transactionCategoriesSums" @click="selectCategoryType(categorySum.category.id)"
|
||||||
class="rounded-xl border p-1 bg-white border-gray-300 mb-2 min-w-fit px-2">
|
class="rounded-xl border p-1 bg-white border-gray-300 mb-2 min-w-fit px-2" :class="selectedCategoryId == categorySum.category.id ? '!bg-blue-100' : ''">
|
||||||
<p><span class="text-sm font-bold">{{ categorySum.category.name }}</span>: {{ categorySum.sum }} ₽</p>
|
<p><span class="text-sm font-bold">{{ categorySum.category.name }}</span>: {{ categorySum.sum }} ₽</p>
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 gap-4 max-h-tlist overflow-y-auto pe-2">
|
<div class="grid grid-cols-1 gap-1 max-h-tlist overflow-y-auto pe-2">
|
||||||
<BudgetTransactionView v-for="transaction in transactions" :key="transaction.id"
|
<BudgetTransactionView v-for="transaction in filteredTransactions" :key="transaction.id"
|
||||||
:transaction="transaction"
|
:transaction="transaction"
|
||||||
:is-list="true"
|
:is-list="true"
|
||||||
@transaction-updated="updateTransactions"
|
@transaction-updated="updateTransactions"
|
||||||
@@ -173,7 +192,7 @@
|
|||||||
<span class="font-light text-sm">+ Добавить</span>
|
<span class="font-light text-sm">+ Добавить</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 mb-2">
|
<div class="grid grid-cols-2 gap-1 mb-2">
|
||||||
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center">
|
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center">
|
||||||
{{ formatAmount(totalIncomes) }}
|
{{ formatAmount(totalIncomes) }}
|
||||||
₽
|
₽
|
||||||
@@ -203,7 +222,7 @@
|
|||||||
<span class="font-light text-sm">+ Добавить</span>
|
<span class="font-light text-sm">+ Добавить</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 mb-2">
|
<div class="grid grid-cols-2 gap-1 mb-2">
|
||||||
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center">
|
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center">
|
||||||
{{ formatAmount(totalPlannedExpenses) }}
|
{{ formatAmount(totalPlannedExpenses) }}
|
||||||
₽
|
₽
|
||||||
@@ -243,14 +262,14 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class=" flex gap-2">
|
<div class=" flex gap-2">
|
||||||
<button v-for="categorySum in transactionCategoriesSums"
|
<button v-for="categorySum in transactionCategoriesSums" @click="selectCategoryType(categorySum.category.id)"
|
||||||
class="rounded-xl border p-1 bg-white border-gray-300 mb-2 min-w-fit px-2">
|
class="rounded-xl border p-1 bg-white border-gray-300 mb-2 min-w-fit px-2" :class="selectedCategoryId == categorySum.category.id ? '!bg-blue-100' : ''">
|
||||||
<p><span class="text-sm font-bold">{{ categorySum.category.name }}</span>: {{ categorySum.sum }} ₽</p>
|
<p><span class="text-sm font-bold">{{ categorySum.category.name }}</span>: {{ categorySum.sum }} ₽</p>
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 gap-4 max-h-tlist overflow-y-auto pe-2">
|
<div class="grid grid-cols-1 gap-1 max-h-tlist overflow-y-auto pe-2">
|
||||||
<BudgetTransactionView v-for="transaction in transactions" :key="transaction.id"
|
<BudgetTransactionView v-for="transaction in filteredTransactions" :key="transaction.id"
|
||||||
:transaction="transaction"
|
:transaction="transaction"
|
||||||
:is-list="true"
|
:is-list="true"
|
||||||
@transaction-updated="updateTransactions"
|
@transaction-updated="updateTransactions"
|
||||||
@@ -306,7 +325,7 @@ import LoadingView from "@/components/LoadingView.vue";
|
|||||||
import ChartDataLabels from 'chartjs-plugin-datalabels';
|
import ChartDataLabels from 'chartjs-plugin-datalabels';
|
||||||
import {Chart as ChartJS} from 'chart.js/auto';
|
import {Chart as ChartJS} from 'chart.js/auto';
|
||||||
import SelectButton from "primevue/selectbutton";
|
import SelectButton from "primevue/selectbutton";
|
||||||
import TransactionForm from "@/components/TransactionForm.vue";
|
import TransactionForm from "@/components/transactions/TransactionForm.vue";
|
||||||
|
|
||||||
// Зарегистрируем плагин
|
// Зарегистрируем плагин
|
||||||
ChartJS.register(ChartDataLabels);
|
ChartJS.register(ChartDataLabels);
|
||||||
@@ -337,6 +356,15 @@ const totalIncomes = computed(() => {
|
|||||||
})
|
})
|
||||||
return totalIncome
|
return totalIncome
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const totalInstantIncomes = computed(() => {
|
||||||
|
let totalIncome = 0;
|
||||||
|
transactions.value.filter(t => t.transactionType.code=='INSTANT' && t.category.type.code =='INCOME' ).forEach((i) => {
|
||||||
|
totalIncome += i.amount
|
||||||
|
})
|
||||||
|
return totalIncome
|
||||||
|
})
|
||||||
|
|
||||||
const totalIncomeLeftToGet = computed(() => {
|
const totalIncomeLeftToGet = computed(() => {
|
||||||
let totalIncomeLeftToGet = 0;
|
let totalIncomeLeftToGet = 0;
|
||||||
plannedIncomes.value.forEach(i => {
|
plannedIncomes.value.forEach(i => {
|
||||||
@@ -357,13 +385,13 @@ const totalLoans = computed(() => {
|
|||||||
|
|
||||||
const loansRatio = computed(() => {
|
const loansRatio = computed(() => {
|
||||||
|
|
||||||
return totalLoans.value / totalExpenses.value * 100
|
return totalExpenses.value == 0? 0 : totalLoans.value / totalExpenses.value * 100
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const savingRatio = computed(() => {
|
const savingRatio = computed(() => {
|
||||||
|
|
||||||
return totalSaving.value / totalExpenses.value * 100
|
return totalExpenses.value == 0? 0 :totalSaving.value / totalExpenses.value * 100
|
||||||
})
|
})
|
||||||
|
|
||||||
const totalSaving = computed(() => {
|
const totalSaving = computed(() => {
|
||||||
@@ -396,7 +424,7 @@ const closeDrawer = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const dailyRatio = computed(() => {
|
const dailyRatio = computed(() => {
|
||||||
const value = (totalExpenses.value - totalLoans.value - totalSaving.value) / totalExpenses.value
|
const value = totalExpenses.value == 0? 0 : (totalExpenses.value - totalLoans.value - totalSaving.value) / totalExpenses.value
|
||||||
|
|
||||||
return value * 100
|
return value * 100
|
||||||
})
|
})
|
||||||
@@ -428,6 +456,14 @@ const totalPlannedExpenses = computed(() => {
|
|||||||
})
|
})
|
||||||
return expenses
|
return expenses
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const totalInstantExpenses = computed(() => {
|
||||||
|
let totalExpenses = 0;
|
||||||
|
transactions.value.filter(t => t.transactionType.code=='INSTANT' && t.category.type.code =='EXPENSE').forEach((i) => {
|
||||||
|
totalExpenses += i.amount
|
||||||
|
})
|
||||||
|
return totalExpenses
|
||||||
|
})
|
||||||
const totalExpenseLeftToSpend = computed(() => {
|
const totalExpenseLeftToSpend = computed(() => {
|
||||||
let totalExpenseLeftToSpend = 0;
|
let totalExpenseLeftToSpend = 0;
|
||||||
plannedExpenses.value.forEach(i => {
|
plannedExpenses.value.forEach(i => {
|
||||||
@@ -442,6 +478,18 @@ const fetchPlannedExpenses = async () => {
|
|||||||
updateLoading.value = false
|
updateLoading.value = false
|
||||||
}
|
}
|
||||||
const transactions = ref<Transaction[]>([])
|
const transactions = ref<Transaction[]>([])
|
||||||
|
|
||||||
|
const selectedCategoryId = ref()
|
||||||
|
const selectCategoryType = (categoryId) => {
|
||||||
|
if (selectedCategoryId.value==categoryId) {
|
||||||
|
selectedCategoryId.value = null
|
||||||
|
} else {
|
||||||
|
selectedCategoryId.value = categoryId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const filteredTransactions = computed(() => {
|
||||||
|
return selectedCategoryId.value ? transactions.value.filter(i => i.category.id==selectedCategoryId.value) : transactions.value
|
||||||
|
})
|
||||||
const fetchBudgetTransactions = async () => {
|
const fetchBudgetTransactions = async () => {
|
||||||
transactions.value = await getBudgetTransactions(route.params.id, 'INSTANT')
|
transactions.value = await getBudgetTransactions(route.params.id, 'INSTANT')
|
||||||
updateLoading.value = false
|
updateLoading.value = false
|
||||||
@@ -740,9 +788,7 @@ onMounted(async () => {
|
|||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid {
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.max-h-tlist {
|
.max-h-tlist {
|
||||||
max-height: 1170px; /* Ограничение высоты списка */
|
max-height: 1170px; /* Ограничение высоты списка */
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="card flex justify-center h-fit">
|
<div class="card flex justify-center h-fit">
|
||||||
<DrawerForm v-if="isDesktop" :visible="visible" :isEditing="isEditing" @close-drawer="closeDrawer">
|
<DrawerForm v-if="isDesktop" :visible="visible" :isEditing="isEditing" @close-drawer="closeDrawer" >
|
||||||
<template #default>
|
<template #default>
|
||||||
<TransactionFormContent :transaction="props.transaction" :transaction-type="transactionType" :category-type="categoryType" @close-drawer="closeDrawer" @create-transaction="transactionUpdated"
|
<TransactionFormContent :transaction="props.transaction" :transaction-type="transactionType" :category-type="categoryType" @close-drawer="closeDrawer" @create-transaction="transactionUpdated"
|
||||||
@delete-transaction="transactionUpdated" @transaction-updated="transactionUpdated" />
|
@delete-transaction="transactionUpdated" @transaction-updated="transactionUpdated" />
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted } from 'vue';
|
||||||
import DrawerForm from "@/components/DrawerForm.vue";
|
import DrawerForm from "@/components/DrawerForm.vue";
|
||||||
import PopUp from "@/components/PopUp.vue";
|
import PopUp from "@/components/PopUp.vue";
|
||||||
import TransactionFormContent from "@/components/TransactionFormContent.vue";
|
import TransactionFormContent from "@/components/transactions/TransactionFormContent.vue";
|
||||||
import {Transaction} from "@/models/Transaction";
|
import {Transaction} from "@/models/Transaction";
|
||||||
|
|
||||||
|
|
||||||
@@ -166,7 +166,7 @@ const createTransaction = async () => {
|
|||||||
editedTransaction.value.isDone = true;
|
editedTransaction.value.isDone = true;
|
||||||
}
|
}
|
||||||
await createTransactionRequest(editedTransaction.value);
|
await createTransactionRequest(editedTransaction.value);
|
||||||
toast.add({severity: 'success', summary: 'Transaction created!', detail: 'Транзакция создана!', life: 3000});
|
toast.add({severity: 'success', summary: 'Транзакция создана!', detail: 'Транзакция создана!', life: 3000});
|
||||||
emit('create-transaction', editedTransaction.value);
|
emit('create-transaction', editedTransaction.value);
|
||||||
computeResult(true)
|
computeResult(true)
|
||||||
resetForm();
|
resetForm();
|
||||||
@@ -213,7 +213,7 @@ const deleteTransaction = async () => {
|
|||||||
try {
|
try {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
await deleteTransactionRequest(editedTransaction.value.id);
|
await deleteTransactionRequest(editedTransaction.value.id);
|
||||||
toast.add({severity: 'success', summary: 'Transaction deleted!', detail: 'Транзакция удалена!', life: 3000});
|
toast.add({severity: 'success', summary: 'Транзакция удалена!', detail: 'Транзакция удалена!', life: 3000});
|
||||||
emit('delete-transaction', editedTransaction.value);
|
emit('delete-transaction', editedTransaction.value);
|
||||||
closeDrawer()
|
closeDrawer()
|
||||||
computeResult(true)
|
computeResult(true)
|
||||||
@@ -230,7 +230,7 @@ const deleteTransaction = async () => {
|
|||||||
// Сброс формы
|
// Сброс формы
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
|
|
||||||
editedTransaction.value.date = new Date();
|
// editedTransaction.value.date = new Date();
|
||||||
editedTransaction.value.amount = null;
|
editedTransaction.value.amount = null;
|
||||||
editedTransaction.value.comment = '';
|
editedTransaction.value.comment = '';
|
||||||
|
|
||||||
@@ -280,10 +280,6 @@ onMounted(async () => {
|
|||||||
<template>
|
<template>
|
||||||
|
|
||||||
<div class="card flex justify-center h-fit">
|
<div class="card flex justify-center h-fit">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div v-if="result" class="absolute top-0 left-0 w-full h-full flex items-center justify-center z-50">
|
<div v-if="result" class="absolute top-0 left-0 w-full h-full flex items-center justify-center z-50">
|
||||||
<div
|
<div
|
||||||
class=" px-10 py-5 rounded-lg border border-gray-200 flex flex-col items-center gap-4"
|
class=" px-10 py-5 rounded-lg border border-gray-200 flex flex-col items-center gap-4"
|
||||||
@@ -312,7 +308,6 @@ onMounted(async () => {
|
|||||||
optionLabel="name"
|
optionLabel="name"
|
||||||
aria-labelledby="basic"
|
aria-labelledby="basic"
|
||||||
class="justify-center"/>
|
class="justify-center"/>
|
||||||
|
|
||||||
<SelectButton v-model="selectedCategoryType" :options="categoryTypes" :allow-empty="false"
|
<SelectButton v-model="selectedCategoryType" :options="categoryTypes" :allow-empty="false"
|
||||||
optionLabel="name"
|
optionLabel="name"
|
||||||
aria-labelledby="basic"
|
aria-labelledby="basic"
|
||||||
@@ -1,47 +1,55 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref } from "vue";
|
||||||
|
|
||||||
import {computed, onMounted, ref} from "vue";
|
|
||||||
import BudgetTransactionView from "@/components/budgets/BudgetTransactionView.vue";
|
import BudgetTransactionView from "@/components/budgets/BudgetTransactionView.vue";
|
||||||
import IconField from "primevue/iconfield";
|
import IconField from "primevue/iconfield";
|
||||||
import InputIcon from "primevue/inputicon";
|
import InputIcon from "primevue/inputicon";
|
||||||
import InputText from "primevue/inputtext";
|
import InputText from "primevue/inputtext";
|
||||||
import {getTransactions} from "@/services/transactionService";
|
import { getTransactions } from "@/services/transactionService";
|
||||||
import {Transaction} from "@/models/Transaction";
|
import { Transaction } from "@/models/Transaction";
|
||||||
|
import ProgressSpinner from "primevue/progressspinner";
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const searchText = ref("");
|
const searchText = ref("");
|
||||||
|
const transactions = ref<Transaction[]>([]);
|
||||||
|
const limit = 20; // Количество транзакций на одну загрузку
|
||||||
|
const offset = ref(0); // Начальное смещение
|
||||||
|
const allLoaded = ref(false); // Флаг для отслеживания окончания данных
|
||||||
|
|
||||||
|
// Функция для получения транзакций с параметрами limit и offset
|
||||||
|
const fetchTransactions = async () => {
|
||||||
|
if (loading.value || allLoaded.value) return; // Останавливаем загрузку, если уже загружается или данные загружены полностью
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const fetchCategories = async () => {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
try {
|
||||||
const response = await getTransactions('INSTANT');
|
const response = await getTransactions('INSTANT', null,null, null, limit, offset.value);
|
||||||
transactions.value = response.data
|
const newTransactions = response.data;
|
||||||
|
|
||||||
|
// Проверка на конец данных
|
||||||
|
if (newTransactions.length < limit) {
|
||||||
|
allLoaded.value = true; // Если данных меньше limit, значит, достигнут конец
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем новые транзакции к текущему списку
|
||||||
|
transactions.value.push(...newTransactions);
|
||||||
|
offset.value += limit; // Обновляем смещение для следующей загрузки
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching categories:', error);
|
console.error("Error fetching transactions:", error);
|
||||||
}
|
}
|
||||||
loading.value = false
|
|
||||||
}
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
const tgname = computed(() => {
|
const tgname = computed(() => {
|
||||||
if (window.Telegram.WebApp) {
|
if (window.Telegram.WebApp) {
|
||||||
const tg = window.Telegram.WebApp;
|
const tg = window.Telegram.WebApp;
|
||||||
// tg.expand(); // Разворачиваем веб-приложение на весь экран
|
return tg.initDataUnsafe.user;
|
||||||
|
|
||||||
// Получаем информацию о пользователе и выводим её
|
|
||||||
return tg.initDataUnsafe.user
|
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
const transactions = ref<Transaction[]>([])
|
|
||||||
|
|
||||||
|
// Отфильтрованные транзакции по поисковому запросу
|
||||||
const filteredTransactions = computed(() => {
|
const filteredTransactions = computed(() => {
|
||||||
if (searchText.value.length === 0) {
|
if (searchText.value.length === 0) {
|
||||||
return transactions.value; // Return the full list when there's no search text
|
return transactions.value;
|
||||||
} else {
|
} else {
|
||||||
return transactions.value.filter(transaction => {
|
return transactions.value.filter(transaction => {
|
||||||
const search = searchText.value.toLowerCase();
|
const search = searchText.value.toLowerCase();
|
||||||
@@ -53,32 +61,47 @@ const filteredTransactions = computed(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Обработчик прокрутки для ленивой загрузки
|
||||||
|
const handleScroll = () => {
|
||||||
|
const bottomReached = window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 2000;
|
||||||
|
if (bottomReached && !loading.value) {
|
||||||
|
fetchTransactions(); // Загружаем следующую страницу
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await fetchCategories();
|
await fetchTransactions(); // Первоначальная загрузка данных
|
||||||
})
|
window.addEventListener("scroll", handleScroll); // Добавляем обработчик прокрутки
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="px-4 bg-gray-100 h-full ">
|
<div class="px-4 bg-gray-100 h-full">
|
||||||
<!-- Заголовок -->
|
<h2 class="text-4xl mb-6 font-bold">Transaction list</h2>
|
||||||
<!-- {{tgname}}-->
|
|
||||||
|
|
||||||
<h2 class="text-4xl mb-6 font-bold">Transaction list</h2>
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<IconField>
|
<IconField>
|
||||||
<InputIcon class="pi pi-search"/>
|
<InputIcon class="pi pi-search"/>
|
||||||
<InputText v-model="searchText" placeholder="Search"></InputText>
|
<InputText v-model="searchText" placeholder="Search"></InputText>
|
||||||
</IconField>
|
</IconField>
|
||||||
|
|
||||||
<div class=" flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<BudgetTransactionView v-for="transaction in filteredTransactions" :transaction="transaction" :is-list="true"/>
|
<BudgetTransactionView
|
||||||
|
v-for="transaction in filteredTransactions"
|
||||||
|
:key="transaction.id"
|
||||||
|
:transaction="transaction"
|
||||||
|
:is-list="true"
|
||||||
|
@transaction-updated="fetchTransactions"
|
||||||
|
/>
|
||||||
|
<!-- Показать спиннер загрузки, если идет загрузка -->
|
||||||
|
<ProgressSpinner v-if="loading" class="mb-4" style="width: 50px; height: 50px;"
|
||||||
|
strokeWidth="8"
|
||||||
|
fill="transparent"
|
||||||
|
animationDuration=".5s" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
</style>
|
||||||
</style>
|
|
||||||
|
|||||||
14
src/main.ts
14
src/main.ts
@@ -23,6 +23,20 @@ app.use(PrimeVue, {
|
|||||||
preset: Aura
|
preset: Aura
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.config.globalProperties.$primevue.config.locale = {
|
||||||
|
firstDayOfWeek: 1, // Устанавливаем понедельник как первый день недели
|
||||||
|
dayNames: ["Воскресенье", "Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота"],
|
||||||
|
dayNamesShort: ["Вс", "Пн", "Вт", "Ср", "Чт", "Пт", "Сб"],
|
||||||
|
dayNamesMin: ["Вс", "Пн", "Вт", "Ср", "Чт", "Пт", "Сб"],
|
||||||
|
monthNames: ["Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"],
|
||||||
|
monthNamesShort: ["Янв", "Фев", "Мар", "Апр", "Май", "Июн", "Июл", "Авг", "Сен", "Окт", "Ноя", "Дек"],
|
||||||
|
today: "Сегодня",
|
||||||
|
clear: "Очистить",
|
||||||
|
dateFormat: "dd.mm.yy",
|
||||||
|
weekHeader: "Нед",
|
||||||
|
};
|
||||||
|
|
||||||
// main.js
|
// main.js
|
||||||
|
|
||||||
if ("serviceWorker" in navigator) {
|
if ("serviceWorker" in navigator) {
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ import SettingsView from "@/components/settings/SettingsView.vue";
|
|||||||
import RecurrentList from "@/components/settings/recurrent/RecurrentList.vue";
|
import RecurrentList from "@/components/settings/recurrent/RecurrentList.vue";
|
||||||
import TransactionList from "@/components/transactions/TransactionList.vue";
|
import TransactionList from "@/components/transactions/TransactionList.vue";
|
||||||
import LoginView from "@/components/auth/LoginView.vue";
|
import LoginView from "@/components/auth/LoginView.vue";
|
||||||
|
import AnalyticsView from "@/components/analytics/AnalyticsView.vue";
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{path: '/login', component: LoginView},
|
{path: '/login', component: LoginView},
|
||||||
{path: '/', name: 'Budgets main', component: BudgetList, meta: {requiresAuth: true}},
|
{path: '/', name: 'Budgets main', component: BudgetList, meta: {requiresAuth: true}},
|
||||||
{path: '/analytics', name: 'Analytics', component: BudgetList, meta: {requiresAuth: true}},
|
{path: '/analytics', name: 'Analytics', component: AnalyticsView, meta: {requiresAuth: true}},
|
||||||
{path: '/budgets', name: 'Budgets', component: BudgetList, meta: {requiresAuth: true}},
|
{path: '/budgets', name: 'Budgets', component: BudgetList, meta: {requiresAuth: true}},
|
||||||
{path: '/budgets/:id', name: 'BudgetView', component: BudgetView, meta: {requiresAuth: true}},
|
{path: '/budgets/:id', name: 'BudgetView', component: BudgetView, meta: {requiresAuth: true}},
|
||||||
{path: '/transactions/:mode*', name: 'Transaction List', component: TransactionList, meta: {requiresAuth: true}},
|
{path: '/transactions/:mode*', name: 'Transaction List', component: TransactionList, meta: {requiresAuth: true}},
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export const getTransaction = async (transactionId: int) => {
|
|||||||
return await apiClient.post(`/transactions/${transactionId}`,);
|
return await apiClient.post(`/transactions/${transactionId}`,);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTransactions = async (transaction_type = null, category_type = null, category_id = null, user_id = null) => {
|
export const getTransactions = async (transaction_type = null, category_type = null, category_id = null, user_id = null, limit = null, offset = null) => {
|
||||||
const params = {};
|
const params = {};
|
||||||
|
|
||||||
// Add the parameters to the params object if they are not null
|
// Add the parameters to the params object if they are not null
|
||||||
@@ -25,6 +25,12 @@ export const getTransactions = async (transaction_type = null, category_type = n
|
|||||||
if (user_id) {
|
if (user_id) {
|
||||||
params.user_id = user_id
|
params.user_id = user_id
|
||||||
}
|
}
|
||||||
|
if (limit) {
|
||||||
|
params.limit = limit
|
||||||
|
}
|
||||||
|
if (offset) {
|
||||||
|
params.offset = offset
|
||||||
|
}
|
||||||
|
|
||||||
// Use axios to make the GET request, passing the params as the second argument
|
// Use axios to make the GET request, passing the params as the second argument
|
||||||
return await apiClient.get('/transactions/', {
|
return await apiClient.get('/transactions/', {
|
||||||
@@ -34,7 +40,10 @@ export const getTransactions = async (transaction_type = null, category_type = n
|
|||||||
|
|
||||||
export const createTransactionRequest = async (transaction: Transaction) => {
|
export const createTransactionRequest = async (transaction: Transaction) => {
|
||||||
transaction.date = format(transaction.date, 'yyyy-MM-dd')
|
transaction.date = format(transaction.date, 'yyyy-MM-dd')
|
||||||
return await apiClient.post('/transactions', transaction);
|
let transactionResponse = await apiClient.post('/transactions', transaction);
|
||||||
|
console.log(transaction.date)
|
||||||
|
transaction.date = new Date(transaction.date);
|
||||||
|
return transactionResponse.data
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateTransactionRequest = async (transaction: Transaction) => {
|
export const updateTransactionRequest = async (transaction: Transaction) => {
|
||||||
@@ -63,4 +72,9 @@ export const deleteTransactionRequest = async (id: number) => {
|
|||||||
|
|
||||||
export const getTransactionTypes = async () => {
|
export const getTransactionTypes = async () => {
|
||||||
return await apiClient.get('/transactions/types');
|
return await apiClient.get('/transactions/types');
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTransactionCategoriesSums = async () => {
|
||||||
|
let response = await apiClient.get('/transactions/categories/_calc_sums');
|
||||||
|
return response.data;
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,6 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
user.value = null;
|
user.value = null;
|
||||||
} finally {
|
} finally {
|
||||||
loadingUser.value = false; // Сбрасываем флаг `loadingUser` в `false` после завершения
|
loadingUser.value = false; // Сбрасываем флаг `loadingUser` в `false` после завершения
|
||||||
console.log('Загрузка завершена, loadingUser:', loadingUser.value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,4 +43,17 @@ export const getMonthName = (month: number) => {
|
|||||||
case 11:
|
case 11:
|
||||||
return 'Декабрь'
|
return 'Декабрь'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const generateRandomColors = () => {
|
||||||
|
|
||||||
|
const r = Math.floor(Math.random() * 256);
|
||||||
|
const g = Math.floor(Math.random() * 256);
|
||||||
|
const b = Math.floor(Math.random() * 256);
|
||||||
|
const a = 0.5; // Прозрачность от 0.00 до 1.00
|
||||||
|
|
||||||
|
return [r,g,b]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пример использования
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user