fixes
This commit is contained in:
68
package-lock.json
generated
68
package-lock.json
generated
@@ -15,6 +15,7 @@
|
||||
"chartjs-plugin-datalabels": "^2.2.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"pinia": "^2.2.6",
|
||||
"platform": "^1.3.6",
|
||||
"primeicons": "^7.0.0",
|
||||
"primevue": "^4.1.0",
|
||||
@@ -6391,6 +6392,56 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pinia": {
|
||||
"version": "2.2.6",
|
||||
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.2.6.tgz",
|
||||
"integrity": "sha512-vIsR8JkDN5Ga2vAxqOE2cJj4VtsHnzpR1Fz30kClxlh0yCHfec6uoMeM3e/ddqmwFUejK3NlrcQa/shnpyT4hA==",
|
||||
"dependencies": {
|
||||
"@vue/devtools-api": "^6.6.3",
|
||||
"vue-demi": "^0.14.10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/posva"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.4.0",
|
||||
"typescript": ">=4.4.4",
|
||||
"vue": "^2.6.14 || ^3.5.11"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pinia/node_modules/vue-demi": {
|
||||
"version": "0.14.10",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.0.0-rc.1",
|
||||
"vue": "^3.0.0-0 || ^2.6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pirates": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
|
||||
@@ -14173,6 +14224,23 @@
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
||||
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="
|
||||
},
|
||||
"pinia": {
|
||||
"version": "2.2.6",
|
||||
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.2.6.tgz",
|
||||
"integrity": "sha512-vIsR8JkDN5Ga2vAxqOE2cJj4VtsHnzpR1Fz30kClxlh0yCHfec6uoMeM3e/ddqmwFUejK3NlrcQa/shnpyT4hA==",
|
||||
"requires": {
|
||||
"@vue/devtools-api": "^6.6.3",
|
||||
"vue-demi": "^0.14.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue-demi": {
|
||||
"version": "0.14.10",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||
"requires": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"pirates": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"chartjs-plugin-datalabels": "^2.2.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"pinia": "^2.2.6",
|
||||
"platform": "^1.3.6",
|
||||
"primeicons": "^7.0.0",
|
||||
"primevue": "^4.1.0",
|
||||
|
||||
39
src/App.vue
39
src/App.vue
@@ -1,17 +1,21 @@
|
||||
<template>
|
||||
|
||||
<div id="app" class="flex flex-col h-screen bg-gray-100 gap-4">
|
||||
<div id="app" class="flex flex-col h-screen bg-gray-300">
|
||||
<!-- MenuBar всегда фиксирован сверху -->
|
||||
<MenuBar class="w-full sticky hidden lg:block top-0 z-10"/>
|
||||
<ToolBar class=" fixed visible lg:invisible bottom-0 z-10"/>
|
||||
|
||||
<!-- Контентная часть заполняет оставшееся пространство -->
|
||||
<div class="flex-grow ">
|
||||
<!-- {{ tg_id }}-->
|
||||
<div class="flex-grow gap-4 ">
|
||||
<!-- {{ tg_id }}-->
|
||||
<Button label="Sub" :class="checkNotif ? 'flex' : '!hidden'" @click="checkSubscribe"/>
|
||||
<router-view/>
|
||||
<div class="bg-gray-100 h-12 block lg:hidden"></div>
|
||||
</div>
|
||||
<!-- <OverlayView class="w-full sticky invisible lg:visible top-0 z-10"/>-->
|
||||
|
||||
<TransactionForm v-if="visible" :visible="visible"
|
||||
:transaction-type="drawerStore.transactionType ? drawerStore.transactionType : 'INSTANT'"
|
||||
:category-type="drawerStore.categoryType ? drawerStore.categoryType : 'EXPENSE'" @close-drawer="closeDrawer"/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
@@ -23,6 +27,16 @@ import Button from "primevue/button";
|
||||
import {computed, onMounted} from "vue";
|
||||
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";
|
||||
|
||||
|
||||
const drawerStore = useDrawerStore();
|
||||
const visible = computed(() => drawerStore.visible);
|
||||
const closeDrawer = () => {
|
||||
drawerStore.setVisible(false);
|
||||
};
|
||||
|
||||
const checkNotif = computed(() => {
|
||||
|
||||
@@ -65,11 +79,20 @@ const sendSubscribe = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
console.log('vyzyvaem app')
|
||||
const userStore = useUserStore();
|
||||
const user = computed(() => userStore.user);
|
||||
console.log('vyzvali app')
|
||||
|
||||
onMounted(async () => {
|
||||
await checkSubscribe()
|
||||
|
||||
|
||||
})
|
||||
console.log("Загружаем данные при монтировании...");
|
||||
if (!userStore.user) {
|
||||
console.log('vyzyvaem app2')
|
||||
await userStore.fetchUserProfile();
|
||||
console.log('vyzvali app2')
|
||||
}
|
||||
await checkSubscribe();
|
||||
});
|
||||
|
||||
|
||||
// @Options({
|
||||
|
||||
19
src/components/DrawerForm.vue
Normal file
19
src/components/DrawerForm.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<Drawer :visible="visible" :header="isEditing ? 'Изменить транзакцию ' : 'Создать транзакцию'" :showCloseIcon="false"
|
||||
position="right" @hide="closeDrawer"
|
||||
class="!w-128 hidden lg:block ">
|
||||
<slot />
|
||||
</Drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Drawer from "primevue/drawer";
|
||||
const props = defineProps({
|
||||
visible: Boolean,
|
||||
isEditing: Boolean,
|
||||
});
|
||||
const emits = defineEmits(['close-drawer']);
|
||||
function closeDrawer() {
|
||||
emits('close-drawer');
|
||||
}
|
||||
</script>
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
<template>
|
||||
<div class="card ">
|
||||
<div v-if="!loadingUser" class="card ">
|
||||
<Menubar :model="items" >
|
||||
<template #start>
|
||||
<!-- <svg width="35" height="40" viewBox="0 0 35 40" fill="none" xmlns="http://www.w3.org/2000/svg" class="h-8">-->
|
||||
@@ -23,9 +23,14 @@
|
||||
<span v-if="item.shortcut" class="ml-auto border border-surface rounded bg-emphasis text-muted-color text-xs p-1">{{ item.shortcut }}</span>
|
||||
<i v-if="hasSubmenu" :class="['pi pi-angle-down', { 'pi-angle-down ml-2': root, 'pi-angle-right ml-auto': !root }]"></i>
|
||||
</router-link>
|
||||
|
||||
</template>
|
||||
|
||||
<template #end>
|
||||
<div class="flex items-center gap-2">
|
||||
{{ user.firstName }}
|
||||
<Button @click="drawerStore.visible = true" label="Create"/>
|
||||
|
||||
<!-- <InputText placeholder="Search" type="text" class="w-32 sm:w-auto" />-->
|
||||
<!-- <Avatar image="https://primefaces.org/cdn/primevue/images/avatar/amyelsner.png" shape="circle" />-->
|
||||
</div>
|
||||
@@ -36,9 +41,23 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import {computed, ref} from "vue";
|
||||
import Badge from "primevue/badge";
|
||||
import Button from "primevue/button"
|
||||
import Menubar from "primevue/menubar";
|
||||
import {useUserStore} from "@/stores/userStore";
|
||||
import {useDrawerStore} from "@/stores/drawerStore.ts";
|
||||
|
||||
|
||||
const userStore = useUserStore()
|
||||
const user = computed(() => userStore.user);
|
||||
const loadingUser = computed(() => userStore.loadingUser);
|
||||
|
||||
const drawerStore = useDrawerStore()
|
||||
const visible = computed(() => drawerStore.visible);
|
||||
|
||||
|
||||
|
||||
|
||||
const items = ref([
|
||||
{
|
||||
|
||||
35
src/components/PopUp.vue
Normal file
35
src/components/PopUp.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<!-- Фон затемнения, отображается только при открытом popup -->
|
||||
<div v-if="isOpen" class="fixed inset-0 bg-black bg-opacity-50 z-40" @click="closePopup"></div>
|
||||
|
||||
<!-- Контейнер popup с анимацией -->
|
||||
<transition name="slide-up">
|
||||
<div v-if="isOpen" class="fixed bottom-0 left-0 right-0 h-[90vh] bg-white rounded-t-2xl shadow-lg z-50 flex flex-col">
|
||||
<header class="flex justify-between items-center p-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold">{{ props.header }}</h2>
|
||||
<button class="text-blue-600 text-lg" @click="closePopup">Закрыть</button>
|
||||
</header>
|
||||
<div class="flex-grow overflow-y-auto p-4">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
header: String
|
||||
})
|
||||
|
||||
const isOpen = ref(true); // Управление открытием popup (можно связать с пропсом)
|
||||
const emits = defineEmits(['close-popup']);
|
||||
function closePopup() {
|
||||
emits('close-popup');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
25
src/components/StatusView.vue
Normal file
25
src/components/StatusView.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
const props = defineProps({
|
||||
isError: Boolean,
|
||||
message: String,
|
||||
show: Boolean,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="show" 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"
|
||||
:class="isError ? 'bg-red-100' : 'bg-green-100'"
|
||||
aria-label="Custom ProgressSpinner">
|
||||
<i class="pi pi-check " :class="isError ? 'text-red-500' : 'text-green-500'" style="font-size: 2rem;"/>
|
||||
<p class="text-green-700" :class="isError ? 'text-red-500' : 'text-green-500'">{{ message }}</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -2,14 +2,8 @@
|
||||
<div
|
||||
class=" items-center toolbar-example justify-between bg-white outline rounded-xl outline-gray-300 shadow-lg h-fit fixed"
|
||||
style="width: 90%; left:5%; bottom: 1.5rem;">
|
||||
<TransactionEditDrawer v-if="drawerOpened" :visible="drawerOpened"
|
||||
|
||||
|
||||
:transaction-type="transactionType"
|
||||
:category-type="categoryType"
|
||||
|
||||
@close-drawer="closeDrawer()"
|
||||
/>
|
||||
<!-- <TransactionForm v-if="drawerOpened" :visible="drawerOpened" :transaction-type="'INSTANT'"-->
|
||||
<!-- :category-type="'EXPENSE'" @close-drawer="closeDrawer"/>-->
|
||||
<div class="flex flex-row rounded-full px-2 justify-between overflow-x">
|
||||
<div class="flex flex-col gap-2 p-2">
|
||||
<router-link to="/budgets" class="items-center flex flex-col gap-2">
|
||||
@@ -66,10 +60,10 @@
|
||||
<button @click="openDrawer('INSTANT')" class="hover:bg-gray-100 p-2 rounded-lg">
|
||||
<p>Создать текущую</p>
|
||||
</button>
|
||||
<button @click="openDrawer('PLANNED', 'INCOME')" class="hover:bg-gray-100 p-2 rounded-lg">
|
||||
<button @click="openDrawer('PLANNED', 'EXPENSE')" class="hover:bg-gray-100 p-2 rounded-lg">
|
||||
<p class="text-left"> Создать плановый расход</p>
|
||||
</button>
|
||||
<button @click=" openDrawer('PLANNED', 'EXPENSE')" class="hover:bg-gray-100 p-2 rounded-lg">
|
||||
<button @click=" openDrawer('PLANNED', 'INCOME')" class="hover:bg-gray-100 p-2 rounded-lg">
|
||||
<p class="text-left">Создать плановое поступление</p>
|
||||
</button>
|
||||
<router-link to="/settings" class="items-center flex flex-col gap-2 p-2">
|
||||
@@ -89,32 +83,38 @@
|
||||
<script setup lang="ts">
|
||||
import {onMounted, ref} from 'vue';
|
||||
import Button from 'primevue/button';
|
||||
import TransactionEditDrawer from "@/components/budgets/TransactionEditDrawer.vue";
|
||||
import {TransactionType} from "@/models/Transaction";
|
||||
import {CategoryType} from "@/models/Category";
|
||||
import {useRoute} from "vue-router";
|
||||
import {useDrawerStore} from "@/stores/drawerStore";
|
||||
|
||||
const showSubmenu = ref(false);
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const transactionType = ref<TransactionType>()
|
||||
const categoryType = ref<CategoryType>()
|
||||
const drawerOpened = ref(false);
|
||||
|
||||
const drawerStore = useDrawerStore()
|
||||
|
||||
|
||||
const refreshPage = () => {
|
||||
window.location.reload(true)
|
||||
}
|
||||
const openDrawer = (selectedTransactionType = null, selectedCategoryType = null) => {
|
||||
if (selectedTransactionType && selectedCategoryType) {
|
||||
transactionType.value = selectedTransactionType;
|
||||
categoryType.value = selectedCategoryType;
|
||||
// transactionType.value = selectedTransactionType;
|
||||
// categoryType.value = selectedCategoryType;
|
||||
drawerStore.setTransactionType(selectedTransactionType)
|
||||
drawerStore.setCategoryType(selectedTransactionType)
|
||||
} else if (selectedTransactionType) {
|
||||
transactionType.value = selectedTransactionType;
|
||||
categoryType.value = 'EXPENSE'
|
||||
// transactionType.value = selectedTransactionType;
|
||||
// categoryType.value = 'EXPENSE'
|
||||
drawerStore.setTransactionType(selectedTransactionType)
|
||||
drawerStore.setCategoryType('EXPENSE')
|
||||
}
|
||||
|
||||
drawerOpened.value = true;
|
||||
console.log(selectedTransactionType)
|
||||
console.log(selectedCategoryType)
|
||||
drawerStore.setVisible( true)
|
||||
}
|
||||
|
||||
|
||||
|
||||
62
src/components/TransactionForm.vue
Normal file
62
src/components/TransactionForm.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div class="card flex justify-center h-fit">
|
||||
<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" />
|
||||
</template>
|
||||
</DrawerForm>
|
||||
|
||||
<PopUp v-else :header="'Создать транзакцию'" @close-popup="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" />
|
||||
</template>
|
||||
</PopUp>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import DrawerForm from "@/components/DrawerForm.vue";
|
||||
import PopUp from "@/components/PopUp.vue";
|
||||
import TransactionFormContent from "@/components/TransactionFormContent.vue";
|
||||
import {Transaction} from "@/models/Transaction";
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
transaction: {
|
||||
type: Object as Transaction,
|
||||
required: false
|
||||
},
|
||||
transactionType: String,
|
||||
categoryType: String,
|
||||
})
|
||||
|
||||
const isDesktop = ref(window.innerWidth >= 1024);
|
||||
const visible = ref(true); // Устанавливаем true или false для показа/скрытия
|
||||
const isEditing = ref(false); // Определяем, редактирование или создание транзакции
|
||||
|
||||
const emit = defineEmits([ 'close-drawer', 'transaction-updated']);
|
||||
|
||||
|
||||
// Обновляем `isDesktop` при изменении размера экрана
|
||||
function updateIsDesktop() {
|
||||
isDesktop.value = window.innerWidth >= 1024;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', updateIsDesktop);
|
||||
});
|
||||
|
||||
const closeDrawer = () => {
|
||||
console.log("close drawer");
|
||||
visible.value = false;
|
||||
emit('close-drawer');
|
||||
};
|
||||
|
||||
const transactionUpdated = () => {
|
||||
emit("transaction-updated");
|
||||
}
|
||||
|
||||
</script>
|
||||
438
src/components/TransactionFormContent.vue
Normal file
438
src/components/TransactionFormContent.vue
Normal file
@@ -0,0 +1,438 @@
|
||||
<script setup lang="ts">
|
||||
import InputText from "primevue/inputtext";
|
||||
import DatePicker from "primevue/datepicker";
|
||||
import FloatLabel from "primevue/floatlabel";
|
||||
import InputNumber from "primevue/inputnumber";
|
||||
import Button from "primevue/button";
|
||||
import {ref, onMounted, computed} from 'vue';
|
||||
import {Transaction, TransactionType} from "@/models/Transaction";
|
||||
import {CategoryType} from "@/models/Category";
|
||||
import SelectButton from "primevue/selectbutton";
|
||||
import Select from "primevue/select";
|
||||
import platform from 'platform';
|
||||
import {
|
||||
createTransactionRequest,
|
||||
getTransactionTypes,
|
||||
updateTransactionRequest,
|
||||
deleteTransactionRequest, getTransactions
|
||||
} from "@/services/transactionService";
|
||||
import {getCategories, getCategoryTypes} from "@/services/categoryService";
|
||||
import {useToast} from "primevue/usetoast";
|
||||
import LoadingView from "@/components/LoadingView.vue";
|
||||
import BudgetTransactionView from "@/components/budgets/BudgetTransactionView.vue";
|
||||
import {useUserStore} from "@/stores/userStore";
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
transaction: {
|
||||
type: Object as () => Transaction,
|
||||
required: false
|
||||
},
|
||||
transactionType: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
categoryType: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
transactions: {
|
||||
type: Array as () => Array<Transaction>,
|
||||
required: false
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['create-transaction', 'update-transaction', 'delete-transaction', 'close-drawer', 'transaction-updated']);
|
||||
const toast = useToast();
|
||||
const categoryTypeChanged = () => {
|
||||
|
||||
editedTransaction.value.category = selectedCategoryType.value.code == "EXPENSE" ? expenseCategories.value[0] : incomeCategories.value[0];
|
||||
|
||||
}
|
||||
const selectCategory = (category) => {
|
||||
isCategorySelectorOpened.value = false;
|
||||
editedTransaction.value.category = category;
|
||||
};
|
||||
|
||||
|
||||
// Состояние
|
||||
const loading = ref(true);
|
||||
const isEditing = ref(!!props.transaction);
|
||||
const isCategorySelectorOpened = ref(false);
|
||||
const editedTransaction = ref<Transaction | null>(null);
|
||||
|
||||
const selectedCategoryType = ref<CategoryType | null>(null);
|
||||
const selectedTransactionType = ref<TransactionType | null>(null);
|
||||
|
||||
const entireCategories = ref<Category[]>([]);
|
||||
const expenseCategories = ref<Category[]>([]);
|
||||
const incomeCategories = ref<Category[]>([]);
|
||||
const categoryTypes = ref<CategoryType[]>([]);
|
||||
const transactionTypes = ref<TransactionType[]>([]);
|
||||
|
||||
const userStore = useUserStore();
|
||||
const user = computed( () => userStore.user)
|
||||
|
||||
const isReady = computed(() => !loading.value && loadingUser.value)
|
||||
|
||||
// Получение категорий и типов транзакций
|
||||
const fetchCategoriesAndTypes = async () => {
|
||||
try {
|
||||
const [categoriesResponse, categoryTypesResponse, transactionTypesResponse] = await Promise.all([
|
||||
getCategories(),
|
||||
getCategoryTypes(),
|
||||
getTransactionTypes()
|
||||
]);
|
||||
entireCategories.value = categoriesResponse.data;
|
||||
expenseCategories.value = categoriesResponse.data.filter((category: Category) => category.type.code === 'EXPENSE');
|
||||
incomeCategories.value = categoriesResponse.data.filter((category: Category) => category.type.code === 'INCOME');
|
||||
|
||||
categoryTypes.value = categoryTypesResponse.data;
|
||||
transactionTypes.value = transactionTypesResponse.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching categories and types:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const checkForm = () => {
|
||||
const errorMessages = {
|
||||
transactionType: 'Тип транзакции должен быть выбран',
|
||||
category: 'Категория должна быть выбрана',
|
||||
date: 'Дата должна быть выбрана',
|
||||
comment: 'Комментарий должен быть введен',
|
||||
amount: 'Сумма не может быть пустой или 0'
|
||||
};
|
||||
|
||||
if (!editedTransaction.value.transactionType) return showError(errorMessages.transactionType);
|
||||
if (!editedTransaction.value.category) return showError(errorMessages.category);
|
||||
if (!editedTransaction.value.date) return showError(errorMessages.date);
|
||||
if (!editedTransaction.value.comment) return showError(errorMessages.comment);
|
||||
if (!editedTransaction.value.amount || editedTransaction.value.amount === 0) return showError(errorMessages.amount);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// Инициализация данных
|
||||
const prepareData = () => {
|
||||
if (!props.transaction) {
|
||||
editedTransaction.value = new Transaction();
|
||||
editedTransaction.value.transactionType = transactionTypes.value.find(type => type.code === props.transactionType) || transactionTypes.value[0];
|
||||
selectedCategoryType.value = categoryTypes.value.find(type => type.code === props.categoryType) || categoryTypes.value[0];
|
||||
editedTransaction.value.category = props.categoryType === 'EXPENSE' ? expenseCategories.value[0] : incomeCategories.value[0];
|
||||
editedTransaction.value.date = new Date();
|
||||
} else {
|
||||
editedTransaction.value = {...props.transaction};
|
||||
selectedCategoryType.value = editedTransaction.value.category.type;
|
||||
console.log('here')
|
||||
selectedTransactionType.value = editedTransaction.value.transactionType;
|
||||
}
|
||||
|
||||
};
|
||||
const result = ref(false)
|
||||
const isError = ref(false)
|
||||
const resultText = ref('')
|
||||
|
||||
const computeResult = (resultState, error) => {
|
||||
|
||||
if (!resultState && error) {
|
||||
result.value = true;
|
||||
isError.value = true
|
||||
resultText.value = `Ошибка: ${error.message}`
|
||||
} else {
|
||||
|
||||
result.value = true;
|
||||
isError.value = false
|
||||
resultText.value = 'Успех!'
|
||||
}
|
||||
setTimeout(() => {
|
||||
result.value = false
|
||||
resultText.value = ''
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const showError = (message) => {
|
||||
result.value = true;
|
||||
isError.value = true;
|
||||
resultText.value = message;
|
||||
return false;
|
||||
};
|
||||
|
||||
// Создание транзакции
|
||||
const createTransaction = async () => {
|
||||
if (checkForm()) {
|
||||
try {
|
||||
loading.value = true;
|
||||
if (editedTransaction.value.transactionType.code === 'INSTANT') {
|
||||
editedTransaction.value.isDone = true;
|
||||
}
|
||||
await createTransactionRequest(editedTransaction.value);
|
||||
toast.add({severity: 'success', summary: 'Transaction created!', detail: 'Транзакция создана!', life: 3000});
|
||||
emit('create-transaction', editedTransaction.value);
|
||||
computeResult(true)
|
||||
resetForm();
|
||||
} catch (error) {
|
||||
computeResult(false, error)
|
||||
console.error('Error creating transaction:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
setTimeout(() => {
|
||||
result.value = false
|
||||
resultText.value = ''
|
||||
}, 1000)
|
||||
};
|
||||
|
||||
// Обновление транзакции
|
||||
const updateTransaction = async () => {
|
||||
if (checkForm()) {
|
||||
try {
|
||||
loading.value = true;
|
||||
const response = await updateTransactionRequest(editedTransaction.value);
|
||||
response.data;
|
||||
// toast.add({severity: 'success', summary: 'Transaction updated!', detail: 'Транзакция обновлена!', life: 3000});
|
||||
emit('update-transaction', editedTransaction.value);
|
||||
emit('transaction-updated');
|
||||
computeResult(true)
|
||||
} catch (error) {
|
||||
computeResult(false, error)
|
||||
console.error('Error updating transaction:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
setTimeout(() => {
|
||||
result.value = false
|
||||
resultText.value = ''
|
||||
|
||||
}, 1000)
|
||||
};
|
||||
|
||||
// Удаление транзакции
|
||||
const deleteTransaction = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
await deleteTransactionRequest(editedTransaction.value.id);
|
||||
toast.add({severity: 'success', summary: 'Transaction deleted!', detail: 'Транзакция удалена!', life: 3000});
|
||||
emit('delete-transaction', editedTransaction.value);
|
||||
closeDrawer()
|
||||
computeResult(true)
|
||||
} catch (error) {
|
||||
computeResult(false, error)
|
||||
toast.add({severity: 'warn', summary: 'Error!', detail: 'Транзакция обновлена!', life: 3000});
|
||||
|
||||
console.error('Error deleting transaction:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Сброс формы
|
||||
const resetForm = () => {
|
||||
|
||||
editedTransaction.value.date = new Date();
|
||||
editedTransaction.value.amount = null;
|
||||
editedTransaction.value.comment = '';
|
||||
|
||||
};
|
||||
|
||||
const dateErrorMessage = computed(() => {
|
||||
|
||||
if (editedTransaction.value.transactionType.code != 'PLANNED' && editedTransaction.value.date > new Date()) {
|
||||
|
||||
return 'При мгновенных тратах дата должна быть меньше текущей!'
|
||||
} else if (editedTransaction.value.transactionType.code == 'PLANNED' && editedTransaction.value.date < new Date()) {
|
||||
|
||||
return 'При плановых тратах дата должна быть больше текущей!'
|
||||
} else {
|
||||
|
||||
return ''
|
||||
}
|
||||
})
|
||||
|
||||
// Закрытие окна
|
||||
const closeDrawer = () => emit('close-drawer');
|
||||
const keyboardOpen = ref(false);
|
||||
const isMobile = ref(false);
|
||||
const userAgent = ref(null);
|
||||
const transactions = ref<Transaction[]>(props.transactions);
|
||||
// Мониторинг при монтировании
|
||||
onMounted(async () => {
|
||||
|
||||
loading.value = true;
|
||||
|
||||
await fetchCategoriesAndTypes();
|
||||
|
||||
prepareData();
|
||||
|
||||
if ( !isEditing.value) {
|
||||
await getTransactions('INSTANT', 'EXPENSE',null, user.value.id ).then(transactionsResponse => transactions.value = transactionsResponse.data);
|
||||
transactions.value = transactions.value.slice(0,3)
|
||||
console.log(transactions.value.slice(0,3))
|
||||
}
|
||||
loading.value = false;
|
||||
const deviceInfo = platform;
|
||||
isMobile.value = deviceInfo.os.family === 'iOS' || deviceInfo.os.family === 'Android';
|
||||
|
||||
})
|
||||
</script>
|
||||
|
||||
<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"
|
||||
:class="isError ? 'bg-red-100' : 'bg-green-100'"
|
||||
aria-label="Custom ProgressSpinner">
|
||||
<i class="pi pi-check " :class="isError ? 'text-red-500' : 'text-green-500'" style="font-size: 2rem;"/>
|
||||
<p class="text-green-700" :class="isError ? 'text-red-500' : 'text-green-500'">{{ resultText }}</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="absolute w-full h-screen">
|
||||
<!-- Полупрозрачный белый фон -->
|
||||
<!-- <div class="absolute top-0 left-0 w-full h-full bg-white opacity-50 z-0"></div>-->
|
||||
|
||||
<!-- Спиннер поверх -->
|
||||
|
||||
</div>
|
||||
|
||||
<LoadingView v-if="loading"/>
|
||||
<div v-else class=" grid gap-4 w-full ">
|
||||
<div class="relative w-full justify-center justify-items-center ">
|
||||
<div class="flex flex-col justify-items-center gap-2">
|
||||
<div class="flex flex-row gap-2">
|
||||
<Select v-if="!isEditing" v-model="editedTransaction.transactionType" :allow-empty="false"
|
||||
:options="transactionTypes"
|
||||
optionLabel="name"
|
||||
aria-labelledby="basic"
|
||||
class="justify-center"/>
|
||||
|
||||
<SelectButton v-model="selectedCategoryType" :options="categoryTypes" :allow-empty="false"
|
||||
optionLabel="name"
|
||||
aria-labelledby="basic"
|
||||
@change="categoryTypeChanged" class="justify-center"/>
|
||||
</div>
|
||||
<button class="border border-gray-300 rounded-lg w-full z-40"
|
||||
@click="isCategorySelectorOpened = !isCategorySelectorOpened">
|
||||
<div class="flex flex-row items-center pe-4 py-2 ">
|
||||
<div class="flex flex-row justify-between w-full gap-4 px-4 items-center">
|
||||
<p class="text-3xl font-bold text-gray-700 dark:text-gray-400">{{
|
||||
editedTransaction.category.icon
|
||||
}}</p>
|
||||
<div class="flex flex-col items-start justify-items-start justify-around w-full">
|
||||
<p class="font-bold text-start line-clamp-1">{{ editedTransaction.category.name }}</p>
|
||||
<p class="font-light line-clamp-1 items-start text-start">{{
|
||||
editedTransaction.category.description
|
||||
}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span :class="{'rotate-90': isCategorySelectorOpened}"
|
||||
class="pi pi-angle-right transition-transform duration-300 text-5xl"/>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Анимированное открытие списка категорий -->
|
||||
<div v-show="isCategorySelectorOpened"
|
||||
class="absolute left-0 right-0 top-full overflow-y-auto z-50 border-b-4 border-x rounded-b-lg bg-white shadow-lg transition-all duration-500"
|
||||
:class="{ 'max-h-0': !isCategorySelectorOpened, 'max-h-[500px]': isCategorySelectorOpened }">
|
||||
<div class="grid grid-cols-2 mt-2">
|
||||
<button
|
||||
v-for="category in editedTransaction.category.type.code == 'EXPENSE' ? expenseCategories : incomeCategories"
|
||||
:key="category.id" class="border rounded-lg mx-2 mb-2"
|
||||
@click="selectCategory(category)">
|
||||
<div class="flex flex-row justify-between w-full px-2">
|
||||
<p class="text-4xl font-bold text-gray-700 dark:text-gray-400">{{ category.icon }}</p>
|
||||
<div class="flex flex-col items-start justify-items-start justify-around w-full">
|
||||
<p class="font-bold text-start">{{ category.name }}</p>
|
||||
<p class="font-light line-clamp-1 text-start">{{ category.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-4">
|
||||
|
||||
|
||||
<FloatLabel variant="on" class="">
|
||||
<InputNumber class=""
|
||||
:invalid="!editedTransaction.amount"
|
||||
:minFractionDigits="0"
|
||||
id="amount"
|
||||
v-model="editedTransaction.amount"
|
||||
mode="currency"
|
||||
currency="RUB"
|
||||
locale="ru-RU"
|
||||
@focus="keyboardOpen=true"
|
||||
@blur="keyboardOpen=false"
|
||||
|
||||
/>
|
||||
<label for="amount" class="">Сумма</label>
|
||||
</FloatLabel>
|
||||
|
||||
<!-- Comment Input -->
|
||||
|
||||
<FloatLabel variant="on" class="w-full">
|
||||
<label for="comment">Комментарий</label>
|
||||
<InputText class="w-full"
|
||||
:invalid="!editedTransaction.comment"
|
||||
id="comment"
|
||||
v-model="editedTransaction.comment"
|
||||
@focus="keyboardOpen=true"
|
||||
@blur="keyboardOpen=false"
|
||||
/>
|
||||
</FloatLabel>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Date Picker -->
|
||||
<div class="field col-12 gap-0">
|
||||
<FloatLabel variant="on">
|
||||
<label for="date">Дата</label>
|
||||
|
||||
<DatePicker class="w-full"
|
||||
inline
|
||||
:invalid="editedTransaction.transactionType.code != 'PLANNED' ? editedTransaction.date > new Date() : true"
|
||||
id="date"
|
||||
v-model="editedTransaction.date"
|
||||
dateFormat="yy-mm-dd"
|
||||
showIcon
|
||||
|
||||
/>
|
||||
<p :class="dateErrorMessage != '' ? 'visible' : 'invisible'"
|
||||
class="text-red-400">{{ dateErrorMessage }}</p>
|
||||
|
||||
|
||||
</FloatLabel>
|
||||
</div>
|
||||
<div>
|
||||
<BudgetTransactionView v-if="!isEditing && transactions" v-for="transaction in transactions" :is-list="true"
|
||||
:transaction="transaction"/>
|
||||
</div>
|
||||
|
||||
<div class="fixed col-12 flex justify-content-end gap-4 bottom-8">
|
||||
|
||||
<Button label="Сохранить" icon="pi pi-check" class="p-button-success"
|
||||
@click="isEditing ? updateTransaction() : createTransaction()"/>
|
||||
<Button label="Отмена" icon="pi pi-times" class="p-button-secondary " @click="closeDrawer"/>
|
||||
<Button v-if="isEditing" label="Удалить" icon="pi pi-times" class="p-button-success" severity="danger"
|
||||
@click="deleteTransaction"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
99
src/components/budgets/BudgetCreationView.vue
Normal file
99
src/components/budgets/BudgetCreationView.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<script setup lang="ts">
|
||||
import Dialog from "primevue/dialog";
|
||||
import Checkbox from "primevue/checkbox";
|
||||
import Button from "primevue/button";
|
||||
import InputText from "primevue/inputtext";
|
||||
import FloatLabel from "primevue/floatlabel";
|
||||
import DatePicker from "primevue/datepicker";
|
||||
import {onMounted, ref} from "vue";
|
||||
import {getMonthName} from "@/utils/utils";
|
||||
import {Budget} from "@/models/Budget";
|
||||
import {createBudget} from "@/services/budgetsService";
|
||||
|
||||
const props = defineProps({
|
||||
opened: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
const emits = defineEmits(['close-modal'])
|
||||
const createRecurrentPayments = ref<Boolean>(true)
|
||||
|
||||
const name = ref('')
|
||||
const dateFrom = ref(new Date())
|
||||
const dateTo = ref(new Date())
|
||||
|
||||
const budget = ref(new Budget())
|
||||
|
||||
const create = async () => {
|
||||
console.log(budget.value)
|
||||
try {
|
||||
await createBudget(budget.value, createRecurrentPayments.value)
|
||||
emits("close-modal");
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const cancel = () => {
|
||||
emits("close-modal");
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
budget.value.name = ''
|
||||
budget.value.dateTo = new Date();
|
||||
budget.value.dateFrom = new Date();
|
||||
budget.value.dateFrom.setDate(10)
|
||||
if (budget.value.dateFrom.getMonth() == 11) {
|
||||
budget.value.dateFrom.setMonth(0)
|
||||
} else {
|
||||
budget.value.dateFrom.setMonth(dateFrom.value.getMonth() + 1)
|
||||
}
|
||||
budget.value.dateTo.setDate(9)
|
||||
if (budget.value.dateTo.getMonth() == 10) {
|
||||
budget.value.dateTo.setMonth(0)
|
||||
budget.value.dateTo.setYear(dateTo.value.getFullYear() + 1)
|
||||
} else {
|
||||
budget.value.dateTo.setMonth(budget.value.dateTo.getMonth() + 2)
|
||||
}
|
||||
budget.value.name = getMonthName(budget.value.dateFrom.getMonth()) + ' ' + budget.value.dateFrom.getFullYear();
|
||||
|
||||
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog :visible="opened" modal header="Создать новый бюджет" :style="{ width: '25rem' }">
|
||||
<div class="flex flex-col gap-4 mt-1">
|
||||
<FloatLabel variant="on" class="w-full">
|
||||
<label for="name">Название</label>
|
||||
<InputText v-model="budget.name" id="name" class="w-full"/>
|
||||
</FloatLabel>
|
||||
<div class="flex flex-row gap-4">
|
||||
<FloatLabel variant="on">
|
||||
<label for="dateFrom">Дата начала</label>
|
||||
<DatePicker v-model="budget.dateFrom" id="dateFrom" dateFormat="dd.mm.yy"/>
|
||||
</FloatLabel>
|
||||
<FloatLabel variant="on">
|
||||
<label for="dateTo">Дата завершения</label>
|
||||
<DatePicker v-model="budget.dateTo" id="dateTo" dateFormat="dd.mm.yy"/>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
<div class="flex flex-row items-center min-w-fit gap-4">
|
||||
<Checkbox v-model="createRecurrentPayments" binary/>
|
||||
Создать ежемесячные платежи?
|
||||
</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"/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,13 +1,18 @@
|
||||
<template>
|
||||
<LoadingView v-if="loading"/>
|
||||
<div v-else class="px-4 bg-gray-100 h-full ">
|
||||
<div v-else class="px-4 bg-gray-100 h-full flex flex-col gap-4 ">
|
||||
<!-- Заголовок -->
|
||||
<h2 class="text-4xl mb-6 font-bold">Monthly Budgets</h2>
|
||||
|
||||
<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() "/>
|
||||
<StatusView :show="creationSuccessModal" :is-error="false" :message="'Бюджет создан!'"/>
|
||||
</div>
|
||||
<!-- Плитка с бюджетами -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<!-- Будущие и текущие бюджеты -->
|
||||
<div v-for="budget in budgetInfos" :key="budget.id" class="p-4 shadow-lg rounded-lg bg-white" :class="budget.dateTo < new Date() ? 'bg-gray-100 opacity-60' : ''">
|
||||
<div v-for="budget in budgetInfos" :key="budget.id" class="p-4 shadow-lg rounded-lg bg-white"
|
||||
:class="budget.dateTo < new Date() ? 'bg-gray-100 opacity-60' : ''">
|
||||
<div class="flex flex-row justify-between">
|
||||
<div class="text-xl font-bold mb-2">{{ budget.name }}</div>
|
||||
<router-link :to="'/budgets/'+budget.id">
|
||||
@@ -18,14 +23,14 @@
|
||||
{{ formatDate(budget.dateFrom) }} - {{ formatDate(budget.dateTo) }}
|
||||
</div>
|
||||
<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">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:
|
||||
<!-- <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>
|
||||
@@ -57,36 +62,25 @@ 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";
|
||||
import {formatDate} from "@/utils/utils";
|
||||
import LoadingView from "@/components/LoadingView.vue";
|
||||
import Button from "primevue/button";
|
||||
import BudgetCreationView from "@/components/budgets/BudgetCreationView.vue";
|
||||
import StatusView from "@/components/StatusView.vue";
|
||||
|
||||
|
||||
const loading = ref(false)
|
||||
const budgetInfos = ref<BudgetInfo[]>([])
|
||||
|
||||
const upcomingBudgets = ref([
|
||||
{
|
||||
id: 1,
|
||||
month: 'October 2024',
|
||||
startDate: '2024-10-01',
|
||||
endDate: '2024-10-31',
|
||||
totalIncome: '500,000 RUB',
|
||||
totalExpenses: '350,000 RUB',
|
||||
plannedExpenses: '300,000 RUB',
|
||||
remainingForUnplanned: '50,000 RUB',
|
||||
unplannedProgress: 60, // Прогресс в процентах
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
month: 'November 2024',
|
||||
startDate: '2024-11-01',
|
||||
endDate: '2024-11-30',
|
||||
totalIncome: '550,000 RUB',
|
||||
totalExpenses: '320,000 RUB',
|
||||
plannedExpenses: '250,000 RUB',
|
||||
remainingForUnplanned: '70,000 RUB',
|
||||
unplannedProgress: 50,
|
||||
},
|
||||
]);
|
||||
const creationOpened = ref(false)
|
||||
const creationSuccessModal = ref(false)
|
||||
const creationSuccessShow = async () => {
|
||||
budgetInfos.value = await getBudgetInfos()
|
||||
creationSuccessModal.value = true
|
||||
setTimeout(() => {
|
||||
creationSuccessModal.value = false
|
||||
}
|
||||
, 1000)
|
||||
}
|
||||
const pastBudgets = ref([
|
||||
{
|
||||
id: 3,
|
||||
@@ -112,6 +106,8 @@ const pastBudgets = ref([
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
loading.value = true;
|
||||
budgetInfos.value = await getBudgetInfos()
|
||||
|
||||
@@ -4,11 +4,11 @@ import Button from "primevue/button";
|
||||
import Checkbox from "primevue/checkbox";
|
||||
import {computed, onMounted, PropType, ref} from "vue";
|
||||
import {Transaction} from "@/models/Transaction";
|
||||
import TransactionEditDrawer from "@/components/budgets/TransactionEditDrawer.vue";
|
||||
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";
|
||||
|
||||
|
||||
const props = defineProps(
|
||||
@@ -135,14 +135,12 @@ onMounted(async () => {
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<TransactionEditDrawer v-if="drawerOpened" :visible="drawerOpened" :expenseCategories="expenseCategories"
|
||||
:incomeCategories="incomeCategories" :transaction="transaction"
|
||||
:category-types="categoryTypes"
|
||||
@transaction-updated="transactionUpdate"
|
||||
@delete-transaction="transactionUpdate"
|
||||
@create-transaction="transactionUpdate"
|
||||
@close-drawer="closeDrawer()"
|
||||
/>
|
||||
|
||||
|
||||
<TransactionForm v-if="drawerOpened" :visible="drawerOpened" :transaction="transaction"
|
||||
@close-drawer="closeDrawer" @transaction-updated="transactionUpdate"
|
||||
@delete-transaction="transactionUpdate"
|
||||
@create-transaction="transactionUpdate"/>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
<div class="flex flex-col ">
|
||||
<!-- {{ budget }}-->
|
||||
<h2 class="text-4xl font-bold">Бюджет {{ budget.name }} </h2>
|
||||
<div class="flex flex-row gap-2 text-xl">{{ formatDate(budget.dateFrom) }} -
|
||||
{{ formatDate(budget.dateTo) }}
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 text-xl">{{ formatDate(budget.dateFrom) }} -
|
||||
{{ formatDate(budget.dateTo) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<!-- Аналитика и плановые доходы/расходы -->
|
||||
@@ -58,8 +58,9 @@
|
||||
</div>
|
||||
<div class="flex flex-col items-center ">
|
||||
<h4 class="text-lg font-bold ">Расходы</h4>
|
||||
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center" :class="totalExpenses > totalIncomes ? ' text-red-700' : ''">
|
||||
-{{ formatAmount(totalExpenses) }} ({{formatAmount(totalExpenses- totalIncomes)}})
|
||||
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center"
|
||||
:class="totalExpenses > totalIncomes ? ' text-red-700' : ''">
|
||||
-{{ formatAmount(totalExpenses) }} ({{ formatAmount(totalExpenses - totalIncomes) }})
|
||||
₽
|
||||
</div>
|
||||
</div>
|
||||
@@ -108,7 +109,8 @@
|
||||
</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" :class="savingRatio < 30 ? '!font-bold text-red-700' : ''">
|
||||
<div class="font-light bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center"
|
||||
:class="savingRatio < 30 ? '!font-bold text-red-700' : ''">
|
||||
{{ savingRatio.toFixed(0) }} %
|
||||
</div>
|
||||
</div>
|
||||
@@ -261,16 +263,21 @@
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<TransactionEditDrawer v-if="drawerOpened" :visible="drawerOpened"
|
||||
:transaction-type="transactionType"
|
||||
:category-type="categoryType"
|
||||
:transactions="transactions.slice(0,3)"
|
||||
@transaction-updated="updateTransactions"
|
||||
@delete-transaction="updateTransactions"
|
||||
@create-transaction="updateTransactions"
|
||||
@close-drawer="closeDrawer"
|
||||
<!-- <TransactionEditDrawer v-if="drawerOpened" :visible="drawerOpened"-->
|
||||
<!-- :transaction-type="transactionType"-->
|
||||
<!-- :category-type="categoryType"-->
|
||||
<!-- :transactions="transactions.slice(0,3)"-->
|
||||
<!-- @transaction-updated="updateTransactions"-->
|
||||
<!-- @delete-transaction="updateTransactions"-->
|
||||
<!-- @create-transaction="updateTransactions"-->
|
||||
<!-- @close-drawer="closeDrawer"-->
|
||||
|
||||
/>
|
||||
<!-- />-->
|
||||
|
||||
<TransactionForm v-if="drawerOpened" :visible="drawerOpened" :transaction-type="transactionType"
|
||||
:category-type="categoryType" @close-drawer="closeDrawer" @transaction-updated="updateTransactions"
|
||||
@delete-transaction="updateTransactions"
|
||||
@create-transaction="updateTransactions"/>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
@@ -299,7 +306,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 TransactionEditDrawer from "@/components/budgets/TransactionEditDrawer.vue";
|
||||
import TransactionForm from "@/components/TransactionForm.vue";
|
||||
|
||||
// Зарегистрируем плагин
|
||||
ChartJS.register(ChartDataLabels);
|
||||
@@ -529,9 +536,9 @@ const incomesByPeriod = computed(() => {
|
||||
let incomesUntil25 = 0
|
||||
let incomesFrom25 = 0
|
||||
plannedIncomes.value.forEach((i) => {
|
||||
console.log(i.date)
|
||||
|
||||
if (i.date >= budget.value?.dateFrom && i.date <= twentyFour.value) {
|
||||
console.log(i.date)
|
||||
|
||||
incomesUntil25 += i.amount
|
||||
} else {
|
||||
incomesFrom25 += i.amount
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import Drawer from "primevue/drawer";
|
||||
import InputText from "primevue/inputtext";
|
||||
import DatePicker from "primevue/datepicker";
|
||||
import FloatLabel from "primevue/floatlabel";
|
||||
@@ -21,6 +20,8 @@ import {getCategories, getCategoryTypes} from "@/services/categoryService";
|
||||
import {useToast} from "primevue/usetoast";
|
||||
import LoadingView from "@/components/LoadingView.vue";
|
||||
import BudgetTransactionView from "@/components/budgets/BudgetTransactionView.vue";
|
||||
import {useUserStore} from "@/stores/userStore";
|
||||
import DrawerForm from "@/components/DrawerForm.vue";
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
@@ -73,6 +74,11 @@ const incomeCategories = ref<Category[]>([]);
|
||||
const categoryTypes = ref<CategoryType[]>([]);
|
||||
const transactionTypes = ref<TransactionType[]>([]);
|
||||
|
||||
const userStore = useUserStore();
|
||||
const user = computed( () => userStore.user)
|
||||
|
||||
const isReady = computed(() => !loading.value && loadingUser.value)
|
||||
|
||||
// Получение категорий и типов транзакций
|
||||
const fetchCategoriesAndTypes = async () => {
|
||||
try {
|
||||
@@ -260,9 +266,9 @@ onMounted(async () => {
|
||||
await fetchCategoriesAndTypes();
|
||||
|
||||
prepareData();
|
||||
if (!transactions.value && !isEditing.value) {
|
||||
console.log()
|
||||
await getTransactions('INSTANT', 'EXPENSE' ).then(transactionsResponse => transactions.value = transactionsResponse.data);
|
||||
|
||||
if ( !isEditing.value) {
|
||||
await getTransactions('INSTANT', 'EXPENSE',null, user.value.id ).then(transactionsResponse => transactions.value = transactionsResponse.data);
|
||||
transactions.value = transactions.value.slice(0,3)
|
||||
console.log(transactions.value.slice(0,3))
|
||||
}
|
||||
@@ -278,9 +284,8 @@ onMounted(async () => {
|
||||
<div class="card flex justify-center h-dvh">
|
||||
|
||||
|
||||
<Drawer :visible="visible" :header="isEditing ? 'Изменить транзакцию' : 'Создать транзакцию'" :showCloseIcon="false"
|
||||
position="right" @hide="closeDrawer"
|
||||
class="!w-128 ">
|
||||
<DrawerForm>
|
||||
|
||||
<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"
|
||||
@@ -301,11 +306,9 @@ onMounted(async () => {
|
||||
|
||||
<LoadingView v-if="loading"/>
|
||||
<div v-else class=" grid gap-4 w-full ">
|
||||
|
||||
<div class="relative w-full justify-center justify-items-center ">
|
||||
<div class="flex flex-col justify-items-center gap-2">
|
||||
<div class="flex flex-row gap-2">
|
||||
<!-- {{editedTransaction.value.transactionType}}-->
|
||||
<Select v-if="!isEditing" v-model="editedTransaction.transactionType" :allow-empty="false"
|
||||
:options="transactionTypes"
|
||||
optionLabel="name"
|
||||
@@ -427,7 +430,7 @@ onMounted(async () => {
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</Drawer>
|
||||
</DrawerForm>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -8,12 +8,12 @@ import InputIcon from "primevue/inputicon";
|
||||
import InputText from "primevue/inputtext";
|
||||
import {getTransactions} from "@/services/transactionService";
|
||||
import {Transaction} from "@/models/Transaction";
|
||||
import {useRoute} from "vue-router";
|
||||
|
||||
|
||||
const loading = ref(false);
|
||||
const searchText = ref("");
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
|
||||
|
||||
const fetchCategories = async () => {
|
||||
@@ -63,6 +63,7 @@ onMounted(async () => {
|
||||
<div class="px-4 bg-gray-100 h-full ">
|
||||
<!-- Заголовок -->
|
||||
<!-- {{tgname}}-->
|
||||
|
||||
<h2 class="text-4xl mb-6 font-bold">Transaction list</h2>
|
||||
<div class="flex flex-col gap-2">
|
||||
<IconField>
|
||||
@@ -70,11 +71,12 @@ onMounted(async () => {
|
||||
<InputText v-model="searchText" placeholder="Search"></InputText>
|
||||
</IconField>
|
||||
|
||||
<div class="mt-4">
|
||||
<BudgetTransactionView class="mb-2" v-for="transaction in filteredTransactions" :transaction="transaction" :is-list="true"/>
|
||||
<div class=" flex flex-col gap-2">
|
||||
<BudgetTransactionView v-for="transaction in filteredTransactions" :transaction="transaction" :is-list="true"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -9,12 +9,13 @@ import router from './router';
|
||||
import Ripple from "primevue/ripple";
|
||||
import ToastService from 'primevue/toastservice'
|
||||
import Tooltip from 'primevue/tooltip';
|
||||
|
||||
import { createPinia } from 'pinia';
|
||||
|
||||
|
||||
const app = createApp(App);
|
||||
app.use(router);
|
||||
app.use(ToastService);
|
||||
app.use(createPinia())
|
||||
app.directive('ripple', Ripple);
|
||||
app.directive('tooltip', Tooltip);
|
||||
app.use(PrimeVue, {
|
||||
|
||||
8
src/models/User.ts
Normal file
8
src/models/User.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export class User {
|
||||
id: number;
|
||||
username: string;
|
||||
firstName: string;
|
||||
tgId: number;
|
||||
isActive: boolean;
|
||||
regDate: Date;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import apiClient from '@/services/axiosSetup';
|
||||
import {Budget, BudgetCategory} from "@/models/Budget";
|
||||
import {format} from "date-fns";
|
||||
// Импортируете настроенный экземпляр axios
|
||||
|
||||
export const getBudgetInfos = async () => {
|
||||
@@ -31,7 +32,7 @@ export const getBudgetTransactions = async (budgetId, transactionType, categoryT
|
||||
url += '?type=' + transactionType
|
||||
}
|
||||
if (transactionType && categoryType) {
|
||||
url += '/'+transactionType+'/'+categoryType
|
||||
url += '/' + transactionType + '/' + categoryType
|
||||
}
|
||||
// if (!categoryType) {
|
||||
// throw new Error('No CategoryType');
|
||||
@@ -68,3 +69,15 @@ export const getBudgetInfo = async (budget_id: number) => {
|
||||
export const updateBudgetCategoryRequest = async (budget_id, category: BudgetCategory) => {
|
||||
await apiClient.put('/budgets/' + budget_id + '/category', category);
|
||||
}
|
||||
|
||||
export const createBudget = async (budget: Budget, createRecurrent: Boolean) => {
|
||||
budget.dateFrom = format(budget.dateFrom, 'yyyy-MM-dd')
|
||||
budget.dateTo = format(budget.dateTo, 'yyyy-MM-dd')
|
||||
let data = {
|
||||
budget: budget,
|
||||
createRecurrent: createRecurrent
|
||||
}
|
||||
await apiClient.post('/budgets', data);
|
||||
budget.dateFrom = format(budget.dateFrom, 'dd.mm.yy')
|
||||
budget.dateTo = format(budget.dateTo, 'dd.mm.yy')
|
||||
}
|
||||
@@ -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) => {
|
||||
export const getTransactions = async (transaction_type = null, category_type = null, category_id = null, user_id = null) => {
|
||||
const params = {};
|
||||
|
||||
// Add the parameters to the params object if they are not null
|
||||
@@ -22,6 +22,9 @@ export const getTransactions = async (transaction_type = null, category_type = n
|
||||
if (category_id) {
|
||||
params.category_id = category_id;
|
||||
}
|
||||
if (user_id) {
|
||||
params.user_id = user_id
|
||||
}
|
||||
|
||||
// Use axios to make the GET request, passing the params as the second argument
|
||||
return await apiClient.get('/transactions/', {
|
||||
|
||||
22
src/stores/drawerStore.ts
Normal file
22
src/stores/drawerStore.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import {defineStore} from "pinia";
|
||||
import {ref} from "vue";
|
||||
|
||||
export const useDrawerStore = defineStore('drawer', () => {
|
||||
const visible = ref(false);
|
||||
const transactionType = ref(null)
|
||||
const categoryType = ref(null)
|
||||
|
||||
const setVisible = (isVisible: boolean) => {
|
||||
visible.value = isVisible;
|
||||
}
|
||||
|
||||
const setTransactionType = (type: string) => {
|
||||
transactionType.value = type;
|
||||
}
|
||||
|
||||
const setCategoryType = (type: string) => {
|
||||
categoryType.value = type;
|
||||
}
|
||||
return {visible, transactionType, categoryType, setTransactionType, setCategoryType, setVisible}
|
||||
|
||||
})
|
||||
29
src/stores/userStore.ts
Normal file
29
src/stores/userStore.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import apiClient from "@/services/axiosSetup";
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
const user = ref(null);
|
||||
const loadingUser = ref(true);
|
||||
|
||||
async function fetchUserProfile() {
|
||||
// Убираем проверку на `loadingUser`, чтобы не блокировать запрос
|
||||
if (!user.value) {
|
||||
loadingUser.value = true;
|
||||
try {
|
||||
const response = await apiClient.get('/auth/users/me'); // запрос к API для получения данных пользователя
|
||||
if (response.status !== 200) throw new Error('Ошибка загрузки данных пользователя');
|
||||
|
||||
user.value = response.data;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при загрузке данных пользователя:', error);
|
||||
user.value = null;
|
||||
} finally {
|
||||
loadingUser.value = false; // Сбрасываем флаг `loadingUser` в `false` после завершения
|
||||
console.log('Загрузка завершена, loadingUser:', loadingUser.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { user, loadingUser, fetchUserProfile };
|
||||
});
|
||||
@@ -15,3 +15,32 @@ export const formatDate = (date) => {
|
||||
year: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
export const getMonthName = (month: number) => {
|
||||
switch (month) {
|
||||
case 0:
|
||||
return 'Январь'
|
||||
case 1:
|
||||
return 'Февраль'
|
||||
case 2:
|
||||
return 'Март'
|
||||
case 3:
|
||||
return 'Апрель'
|
||||
case 4:
|
||||
return 'Май'
|
||||
case 5:
|
||||
return 'Июнь'
|
||||
case 6:
|
||||
return 'Июль'
|
||||
case 7:
|
||||
return 'Август'
|
||||
case 8:
|
||||
return 'Сентябрь'
|
||||
case 9:
|
||||
return 'Октябрь'
|
||||
case 10:
|
||||
return 'Ноябрь'
|
||||
case 11:
|
||||
return 'Декабрь'
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user