This commit is contained in:
Vladimir Voronin
2024-11-06 19:14:53 +03:00
parent 04fae4dec7
commit 9d546f6069
23 changed files with 1008 additions and 108 deletions

68
package-lock.json generated
View File

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

View File

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

View File

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

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

View File

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

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

View File

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@@ -0,0 +1,8 @@
export class User {
id: number;
username: string;
firstName: string;
tgId: number;
isActive: boolean;
regDate: Date;
}

View File

@@ -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');
@@ -67,4 +68,16 @@ 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')
}

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) => {
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
View 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
View 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 };
});

View File

@@ -14,4 +14,33 @@ export const formatDate = (date) => {
month: '2-digit',
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 'Декабрь'
}
}