fixessome

This commit is contained in:
Vladimir Voronin
2024-11-08 09:59:46 +03:00
parent b7cccecaec
commit c33af74342
21 changed files with 453 additions and 343 deletions

15
package-lock.json generated
View File

@@ -18,6 +18,7 @@
"pinia": "^2.2.6",
"platform": "^1.3.6",
"primeicons": "^7.0.0",
"primelocale": "^1.0.4",
"primevue": "^4.1.0",
"tailwindcss-primeui": "^0.3.4",
"vue": "^3.5.11",
@@ -7155,6 +7156,15 @@
"resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz",
"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": {
"version": "4.1.0",
"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",
"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": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/primevue/-/primevue-4.1.0.tgz",

View File

@@ -18,6 +18,7 @@
"pinia": "^2.2.6",
"platform": "^1.3.6",
"primeicons": "^7.0.0",
"primelocale": "^1.0.4",
"primevue": "^4.1.0",
"tailwindcss-primeui": "^0.3.4",
"vue": "^3.5.11",

View File

@@ -9,7 +9,7 @@
<div class="flex flex-col flex-grow gap-4 ">
<!-- {{ tg_id }}-->
<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>
@@ -29,7 +29,7 @@ import {subscribeUserToPush} from "@/services/pushManager";
import apiClient from '@/services/axiosSetup';
import {useUserStore} from "@/stores/userStore";
import {useDrawerStore} from '@/stores/drawerStore'
import TransactionForm from "@/components/TransactionForm.vue";
import TransactionForm from "@/components/transactions/TransactionForm.vue";
const drawerStore = useDrawerStore();
@@ -79,17 +79,17 @@ const sendSubscribe = async () => {
}
}
console.log('vyzyvaem app')
const userStore = useUserStore();
const user = computed(() => userStore.user);
console.log('vyzvali app')
onMounted(async () => {
console.log("Загружаем данные при монтировании...");
if (!userStore.user) {
console.log('vyzyvaem app2')
await userStore.fetchUserProfile();
console.log('vyzvali app2')
}
await checkSubscribe();
});

View File

@@ -1,6 +1,6 @@
<template>
<Drawer :visible="visible" :header="isEditing ? 'Изменить транзакцию ' : 'Создать транзакцию'" :showCloseIcon="false"
position="right" @hide="closeDrawer"
<Drawer :visible="visible" :header="isEditing ? 'Изменить транзакцию ' : 'Создать транзакцию'" :showCloseIcon="true"
position="right" @hide="closeDrawer" @update:visible="closeDrawer"
class="!w-128 hidden lg:block ">
<slot />
</Drawer>

View File

@@ -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>

View File

@@ -29,7 +29,7 @@
<template #end>
<div class="flex items-center gap-2">
{{ 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" />-->
<!-- <Avatar image="https://primefaces.org/cdn/primevue/images/avatar/amyelsner.png" shape="circle" />-->

View File

@@ -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>
Vues
<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>

View File

@@ -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>

View 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>

View File

@@ -16,7 +16,7 @@ const props = defineProps({
required: true
}
})
const emits = defineEmits(['close-modal'])
const emits = defineEmits(['budget-created','close-modal'])
const createRecurrentPayments = ref<Boolean>(true)
const name = ref('')
@@ -29,7 +29,7 @@ const create = async () => {
console.log(budget.value)
try {
await createBudget(budget.value, createRecurrentPayments.value)
emits("close-modal");
emits("budget-created");
} catch (e) {
console.error(e)
}
@@ -65,7 +65,7 @@ onMounted(() => {
</script>
<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">
<FloatLabel variant="on" class="w-full">
<label for="name">Название</label>
@@ -87,7 +87,7 @@ onMounted(() => {
</div>
<div class="flex flex-row gap-2 justify-end items-center">
<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>
</Dialog>

View File

@@ -5,7 +5,7 @@
<div class="flex flex-row gap-4 items-center">
<h2 class="text-4xl font-bold">Бюджеты</h2>
<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="'Бюджет создан!'"/>
</div>
<!-- Плитка с бюджетами -->
@@ -22,44 +22,43 @@
<div class="text-sm text-gray-600 mb-4">
{{ formatDate(budget.dateFrom) }} - {{ formatDate(budget.dateTo) }}
</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 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">
Unplanned Expenses:
<!-- <div class="text-sm flex items-center">-->
<!-- Unplanned Expenses:-->
<!-- <span class="ml-2 font-bold">{{ formatAmount(budget.totalIncomes - budget.totalExpenses) }} </span>-->
<!-- Прогресс бар -->
<!-- <ProgressBar :value="budget.unplannedProgress" class="ml-4 w-full"/>-->
</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 class="text-xl font-bold mb-2">{{ budget.month }}</div>
<div class="text-sm text-gray-600 mb-4">
{{ budget.startDate }} - {{ budget.endDate }}
</div>
<div class="mb-4">
<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">Planned Expenses: <span class="font-bold">{{ budget.plannedExpenses }}</span></div>
<div class="text-sm flex items-center">
Unplanned Expenses:
<span class="ml-2 font-bold">{{ budget.remainingForUnplanned }}</span>
<!-- Прогресс бар -->
<ProgressBar :value="budget.unplannedProgress" class="ml-4 w-full"/>
</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 class="text-xl font-bold mb-2">{{ budget.month }}</div>-->
<!-- <div class="text-sm text-gray-600 mb-4">-->
<!-- {{ budget.startDate }} - {{ budget.endDate }}-->
<!-- </div>-->
<!-- <div class="mb-4">-->
<!-- <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">Planned Expenses: <span class="font-bold">{{ budget.plannedExpenses }}</span></div>-->
<!-- <div class="text-sm flex items-center">-->
<!-- Unplanned Expenses:-->
<!-- <span class="ml-2 font-bold">{{ budget.remainingForUnplanned }}</span>-->
<!-- &lt;!&ndash; Прогресс бар &ndash;&gt;-->
<!-- <ProgressBar :value="budget.unplannedProgress" class="ml-4 w-full"/>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
</div>
</div>
</template>
<script setup lang="ts">
import {onMounted, ref} from 'vue';
import ProgressBar from 'primevue/progressbar';
import {BudgetInfo} from "@/models/Budget";
import {getBudgetInfos} from "@/services/budgetsService";
import {formatDate} from "@/utils/utils";
@@ -74,6 +73,7 @@ const budgetInfos = ref<BudgetInfo[]>([])
const creationOpened = ref(false)
const creationSuccessModal = ref(false)
const creationSuccessShow = async () => {
creationOpened.value = false
budgetInfos.value = await getBudgetInfos()
creationSuccessModal.value = true
setTimeout(() => {

View File

@@ -8,7 +8,7 @@ import {Category, CategoryType} from "@/models/Category";
import {getCategories, getCategoryTypes} from "@/services/categoryService";
import {setTransactionDoneRequest} from "@/services/transactionService";
import {formatAmount, formatDate} from "@/utils/utils";
import TransactionForm from "@/components/TransactionForm.vue";
import TransactionForm from "@/components/transactions/TransactionForm.vue";
const props = defineProps(

View File

@@ -121,6 +121,26 @@
</div>
</div>
</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 v-for="categorySum in transactionCategoriesSums" class="flex flex-col items-center font-bold ">
<p class="font-light ">{{ categorySum.category.icon }} {{ categorySum.category.name }}</p>
@@ -139,20 +159,19 @@
<div class="flex flex-row gap-4">
<h3 class="text-2xl font-bold">Транзакции</h3>
<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>
</button>
</div>
<div class=" flex gap-2 overflow-x-auto ">
<button v-for="categorySum in transactionCategoriesSums"
class="rounded-xl border p-1 bg-white border-gray-300 mb-2 min-w-fit px-2">
<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="selectedCategoryId == categorySum.category.id ? '!bg-blue-100' : ''">
<p><span class="text-sm font-bold">{{ categorySum.category.name }}</span>: {{ categorySum.sum }} </p>
</button>
</div>
<div class="grid grid-cols-1 gap-4 max-h-tlist overflow-y-auto pe-2">
<BudgetTransactionView v-for="transaction in transactions" :key="transaction.id"
<div class="grid grid-cols-1 gap-1 max-h-tlist overflow-y-auto pe-2">
<BudgetTransactionView v-for="transaction in filteredTransactions" :key="transaction.id"
:transaction="transaction"
:is-list="true"
@transaction-updated="updateTransactions"
@@ -173,7 +192,7 @@
<span class="font-light text-sm">+ Добавить</span>
</button>
</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">
{{ formatAmount(totalIncomes) }}
@@ -203,7 +222,7 @@
<span class="font-light text-sm">+ Добавить</span>
</button>
</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">
{{ formatAmount(totalPlannedExpenses) }}
@@ -243,14 +262,14 @@
</button>
</div>
<div class=" flex gap-2">
<button v-for="categorySum in transactionCategoriesSums"
class="rounded-xl border p-1 bg-white border-gray-300 mb-2 min-w-fit px-2">
<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="selectedCategoryId == categorySum.category.id ? '!bg-blue-100' : ''">
<p><span class="text-sm font-bold">{{ categorySum.category.name }}</span>: {{ categorySum.sum }} </p>
</button>
</div>
<div class="grid grid-cols-1 gap-4 max-h-tlist overflow-y-auto pe-2">
<BudgetTransactionView v-for="transaction in transactions" :key="transaction.id"
<div class="grid grid-cols-1 gap-1 max-h-tlist overflow-y-auto pe-2">
<BudgetTransactionView v-for="transaction in filteredTransactions" :key="transaction.id"
:transaction="transaction"
:is-list="true"
@transaction-updated="updateTransactions"
@@ -306,7 +325,7 @@ import LoadingView from "@/components/LoadingView.vue";
import ChartDataLabels from 'chartjs-plugin-datalabels';
import {Chart as ChartJS} from 'chart.js/auto';
import SelectButton from "primevue/selectbutton";
import TransactionForm from "@/components/TransactionForm.vue";
import TransactionForm from "@/components/transactions/TransactionForm.vue";
// Зарегистрируем плагин
ChartJS.register(ChartDataLabels);
@@ -337,6 +356,15 @@ const totalIncomes = computed(() => {
})
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(() => {
let totalIncomeLeftToGet = 0;
plannedIncomes.value.forEach(i => {
@@ -357,13 +385,13 @@ const totalLoans = computed(() => {
const loansRatio = computed(() => {
return totalLoans.value / totalExpenses.value * 100
return totalExpenses.value == 0? 0 : totalLoans.value / totalExpenses.value * 100
})
const savingRatio = computed(() => {
return totalSaving.value / totalExpenses.value * 100
return totalExpenses.value == 0? 0 :totalSaving.value / totalExpenses.value * 100
})
const totalSaving = computed(() => {
@@ -396,7 +424,7 @@ const closeDrawer = async () => {
}
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
})
@@ -428,6 +456,14 @@ const totalPlannedExpenses = computed(() => {
})
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(() => {
let totalExpenseLeftToSpend = 0;
plannedExpenses.value.forEach(i => {
@@ -442,6 +478,18 @@ const fetchPlannedExpenses = async () => {
updateLoading.value = false
}
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 () => {
transactions.value = await getBudgetTransactions(route.params.id, 'INSTANT')
updateLoading.value = false
@@ -740,9 +788,7 @@ onMounted(async () => {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.grid {
gap: 1rem;
}
.max-h-tlist {
max-height: 1170px; /* Ограничение высоты списка */

View File

@@ -1,6 +1,6 @@
<template>
<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>
<TransactionFormContent :transaction="props.transaction" :transaction-type="transactionType" :category-type="categoryType" @close-drawer="closeDrawer" @create-transaction="transactionUpdated"
@delete-transaction="transactionUpdated" @transaction-updated="transactionUpdated" />
@@ -20,7 +20,7 @@
import { ref, onMounted } from 'vue';
import DrawerForm from "@/components/DrawerForm.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";

View File

@@ -166,7 +166,7 @@ const createTransaction = async () => {
editedTransaction.value.isDone = true;
}
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);
computeResult(true)
resetForm();
@@ -213,7 +213,7 @@ const deleteTransaction = async () => {
try {
loading.value = true;
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);
closeDrawer()
computeResult(true)
@@ -230,7 +230,7 @@ const deleteTransaction = async () => {
// Сброс формы
const resetForm = () => {
editedTransaction.value.date = new Date();
// editedTransaction.value.date = new Date();
editedTransaction.value.amount = null;
editedTransaction.value.comment = '';
@@ -280,10 +280,6 @@ onMounted(async () => {
<template>
<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
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"
aria-labelledby="basic"
class="justify-center"/>
<SelectButton v-model="selectedCategoryType" :options="categoryTypes" :allow-empty="false"
optionLabel="name"
aria-labelledby="basic"

View File

@@ -1,47 +1,55 @@
<script setup lang="ts">
import {computed, onMounted, ref} from "vue";
import { computed, onMounted, ref } from "vue";
import BudgetTransactionView from "@/components/budgets/BudgetTransactionView.vue";
import IconField from "primevue/iconfield";
import InputIcon from "primevue/inputicon";
import InputText from "primevue/inputtext";
import {getTransactions} from "@/services/transactionService";
import {Transaction} from "@/models/Transaction";
import { getTransactions } from "@/services/transactionService";
import { Transaction } from "@/models/Transaction";
import ProgressSpinner from "primevue/progressspinner";
const loading = ref(false);
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 {
const response = await getTransactions('INSTANT');
transactions.value = response.data
const response = await getTransactions('INSTANT', null,null, null, limit, offset.value);
const newTransactions = response.data;
// Проверка на конец данных
if (newTransactions.length < limit) {
allLoaded.value = true; // Если данных меньше limit, значит, достигнут конец
}
// Добавляем новые транзакции к текущему списку
transactions.value.push(...newTransactions);
offset.value += limit; // Обновляем смещение для следующей загрузки
} catch (error) {
console.error('Error fetching categories:', error);
console.error("Error fetching transactions:", error);
}
loading.value = false
}
loading.value = false;
};
const tgname = computed(() => {
if (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(() => {
if (searchText.value.length === 0) {
return transactions.value; // Return the full list when there's no search text
return transactions.value;
} else {
return transactions.value.filter(transaction => {
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 () => {
await fetchCategories();
})
await fetchTransactions(); // Первоначальная загрузка данных
window.addEventListener("scroll", handleScroll); // Добавляем обработчик прокрутки
});
</script>
<template>
<div class="px-4 bg-gray-100 h-full ">
<!-- Заголовок -->
<!-- {{tgname}}-->
<h2 class="text-4xl mb-6 font-bold">Transaction list</h2>
<div class="px-4 bg-gray-100 h-full">
<h2 class="text-4xl mb-6 font-bold">Transaction list</h2>
<div class="flex flex-col gap-2">
<IconField>
<InputIcon class="pi pi-search"/>
<InputText v-model="searchText" placeholder="Search"></InputText>
</IconField>
<div class=" flex flex-col gap-2">
<BudgetTransactionView v-for="transaction in filteredTransactions" :transaction="transaction" :is-list="true"/>
<div class="flex flex-col gap-2">
<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>
</template>
<style scoped>
</style>

View File

@@ -23,6 +23,20 @@ app.use(PrimeVue, {
preset: Aura
}
});
app.config.globalProperties.$primevue.config.locale = {
firstDayOfWeek: 1, // Устанавливаем понедельник как первый день недели
dayNames: ["Воскресенье", "Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота"],
dayNamesShort: ["Вс", "Пн", "Вт", "Ср", "Чт", "Пт", "Сб"],
dayNamesMin: ["Вс", "Пн", "Вт", "Ср", "Чт", "Пт", "Сб"],
monthNames: ["Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"],
monthNamesShort: ["Янв", "Фев", "Мар", "Апр", "Май", "Июн", "Июл", "Авг", "Сен", "Окт", "Ноя", "Дек"],
today: "Сегодня",
clear: "Очистить",
dateFormat: "dd.mm.yy",
weekHeader: "Нед",
};
// main.js
if ("serviceWorker" in navigator) {

View File

@@ -8,11 +8,12 @@ import SettingsView from "@/components/settings/SettingsView.vue";
import RecurrentList from "@/components/settings/recurrent/RecurrentList.vue";
import TransactionList from "@/components/transactions/TransactionList.vue";
import LoginView from "@/components/auth/LoginView.vue";
import AnalyticsView from "@/components/analytics/AnalyticsView.vue";
const routes = [
{path: '/login', component: LoginView},
{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/:id', name: 'BudgetView', component: BudgetView, meta: {requiresAuth: true}},
{path: '/transactions/:mode*', name: 'Transaction List', component: TransactionList, meta: {requiresAuth: true}},

View File

@@ -7,7 +7,7 @@ export const getTransaction = async (transactionId: int) => {
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 = {};
// 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) {
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
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) => {
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) => {
@@ -64,3 +73,8 @@ export const deleteTransactionRequest = async (id: number) => {
export const getTransactionTypes = async () => {
return await apiClient.get('/transactions/types');
}
export const getTransactionCategoriesSums = async () => {
let response = await apiClient.get('/transactions/categories/_calc_sums');
return response.data;
}

View File

@@ -20,7 +20,6 @@ export const useUserStore = defineStore('user', () => {
user.value = null;
} finally {
loadingUser.value = false; // Сбрасываем флаг `loadingUser` в `false` после завершения
console.log('Загрузка завершена, loadingUser:', loadingUser.value);
}
}
}

View File

@@ -44,3 +44,16 @@ export const getMonthName = (month: number) => {
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]
}
// Пример использования