This commit is contained in:
Vladimir Voronin
2025-01-06 16:30:22 +03:00
parent bc7c08cefc
commit e09fe77a5e
29 changed files with 911 additions and 553 deletions

View File

@@ -1,6 +1,7 @@
// public/service-worker.js
self.addEventListener("push", (event) => {
console.log(event)
const data = event.data.json();
console.log(data);
const options = {

View File

@@ -2,13 +2,14 @@
<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"/>
<MenuBar v-if="userStore.user" class="w-full sticky hidden lg:block top-0 z-10"/>
<ToolBar class=" fixed visible lg:invisible bottom-0 z-10"/>
<!-- Контентная часть заполняет оставшееся пространство -->
<div class="flex flex-col flex-grow">
<!-- {{ 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>
@@ -32,6 +33,7 @@ import {useDrawerStore} from '@/stores/drawerStore'
import TransactionForm from "@/components/transactions/TransactionForm.vue";
const drawerStore = useDrawerStore();
const visible = computed(() => drawerStore.visible);
const closeDrawer = () => {
@@ -73,7 +75,7 @@ const sendSubscribe = async () => {
console.log("Push subscription:", subscription);
// Отправка подписки на сервер для хранения
await apiClient.post("/push/subscribe", subscription)
await apiClient.post("/subscriptions/subscribe", subscription)
} catch (error) {
console.error("Failed to subscribe to push:", error);
}

View File

@@ -17,12 +17,43 @@ ChartJS.register(ChartDataLabels);
const loading = ref(false);
const categoriesSums = ref([])
const dataTableCategories = ref([])
const fetchCategoriesSums = async () => {
loading.value = true
try {
categoriesSums.value = await getTransactionCategoriesSums()
// console.log(categoriesSums.value)
for (let category of categoriesSums.value) {
// console.log(category)
// [{
// "category": category[0],
// "category"+[category[1]]: category[2]
// }]
// console.log('test')
// console.log('dataTableCategories '+ dataTableCategories.value)
let categoryInList = dataTableCategories.value.find((listCategory: Category) => {
// console.log(listCategory['category'].id)
// console.log(category[0].id)
return listCategory['category'].id === category[0].id
})
console.log('cat in list ' + categoryInList)
if (categoryInList) {
console.log('cat[1] '+ category[1])
console.log('cat[2] '+ category[2])
categoryInList[category[1]] = category[2]
// console.log(categoryInList)
} else {
dataTableCategories.value.push({'category': category[0]})
dataTableCategories.value.filter((listCategory: Category) => {
return listCategory['category'].id === category.id
})[category[1]] = category[2]
}
// console.log(categoryInList)
}
// console.log(dataTableCategories.value)
// console.log(categoriesSums.value)
} catch (error) {
console.error('Error fetching categories sums:', error);
}
@@ -37,8 +68,8 @@ const fetchCategories = async () => {
try {
const response = await getCategories('EXPENSE');
categories.value = response.data
console.log(categories.value.filter(i => i.id==30))
selectedCategories.value.push(categories.value.filter(i => i.id==30)[0])
console.log(categories.value.filter(i => i.id == 30))
selectedCategories.value.push(categories.value.filter(i => i.id == 30)[0])
} catch (error) {
console.error('Error fetching categories:', error);
}
@@ -213,8 +244,13 @@ onMounted(async () => {
placeholder="Выберите категории"
:maxSelectedLabels="3" class="w-full md:w-80"/>
<Chart v-if="selectedCategories.length > 0" type="line" :data="chartData" :options="chartOptions" class="h-[30rem]"/>
<Chart v-if="selectedCategories.length > 0" type="line" :data="chartData" :options="chartOptions"
class="h-[30rem]"/>
{{dataTableCategories}}
<DataTable :value="dataTableCategories">
<!-- <Column v-for="dataTableCategories"/>-->
</DataTable>
<!-- {{categories}}-->
<!-- {{// chartData}}-->
</div>

View File

@@ -36,10 +36,10 @@
</template>
<script setup lang="ts">
import qs from 'qs';
import { computed, ref, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import apiClient from '@/services/axiosSetup';
import {useUserStore} from "@/stores/userStore";
const username = ref('');
const password = ref('');
@@ -64,7 +64,7 @@ const autoLoginWithTgId = async () => {
const token = response.data.access_token;
localStorage.setItem('token', token);
apiClient.defaults.headers.common['Authorization'] = `Bearer ${token}`;
await router.push(route.query['back'] ? route.query['back'].toString() : '/');
await router.replace(route.query['back']+"/reload" ? route.query['back'].toString() : '/');
} catch (error) {
console.error(error);
alert('Ошибка входа. Проверьте логин и пароль.');
@@ -77,27 +77,29 @@ onMounted(() => {
autoLoginWithTgId();
});
const userStore = useUserStore();
// Основная функция для логина
const login = async () => {
try {
let response;
if (tg_id.value) {
response = await apiClient.post('/auth/token/tg', qs.stringify({ tg_id: tg_id.value }));
} else {
response = await apiClient.post('/auth/token', qs.stringify({
username: username.value,
password: password.value,
}));
}
const token = response.data.access_token;
localStorage.setItem('token', token);
apiClient.defaults.headers.common['Authorization'] = `Bearer ${token}`;
await router.push(route.query['back'] ? route.query['back'].toString() : '/');
} catch (error) {
console.error(error);
alert('Ошибка входа. Проверьте логин и пароль.');
}
await userStore.login(username.value, password.value);
// try {
// let response;
// if (tg_id.value) {
// response = await apiClient.post('/auth/token/tg', qs.stringify({ tg_id: tg_id.value }));
// } else {
// response = await apiClient.post('/auth/login', {
// username: username.value,
// password: password.value,
// });
// }
//
// const token = response.data.token;
// localStorage.setItem('token', token);
// apiClient.defaults.headers.common['Authorization'] = `Bearer ${token}`;
// await router.push(route.query['back'] ? route.query['back'].toString() : '/');
// } catch (error) {
// console.error(error);
// alert('Ошибка входа. Проверьте логин и пароль.');
// }
};
</script>

View File

@@ -13,7 +13,7 @@ const props = defineProps({
required: true
},
budgetId: {
type: Number,
type: String,
required: true
}
});
@@ -36,7 +36,7 @@ const stopEditing = () => {
const spentPlannedRatio = computed(() => {
return props.category.currentLimit
? (props.category.currentSpent / props.category.currentLimit) * 100
: 0;
: props.category.currentSpent > 0 ? props.category.currentSpent : 0;
});
// Синхронизация `currentLimit` с `props.category.currentLimit` при обновлении

View File

@@ -8,7 +8,6 @@ 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: {
@@ -28,10 +27,10 @@ const budget = ref(new Budget())
const create = async () => {
console.log(budget.value)
try {
await createBudget(budget.value, createRecurrentPayments.value)
emits("budget-created");
emits("budget-created", budget.value, createRecurrentPayments.value);
} catch (e) {
console.error(e)
throw e
}
}
@@ -40,7 +39,7 @@ const cancel = () => {
emits("close-modal");
}
onMounted(() => {
const resetForm = () => {
budget.value.name = ''
budget.value.dateTo = new Date();
budget.value.dateFrom = new Date();
@@ -58,8 +57,10 @@ onMounted(() => {
budget.value.dateTo.setMonth(budget.value.dateTo.getMonth() + 2)
}
budget.value.name = getMonthName(budget.value.dateFrom.getMonth()) + ' ' + budget.value.dateFrom.getFullYear();
}
onMounted(() => {
resetForm()
})
</script>

View File

@@ -5,81 +5,106 @@
<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" @budget-created="creationSuccessShow()" @close-modal="creationOpened=false" />
<BudgetCreationView :opened="creationOpened" @budget-created="creationSuccessShow"
@close-modal="creationOpened=false"/>
<StatusView :show="creationSuccessModal" :is-error="false" :message="'Бюджет создан!'"/>
</div>
<!-- Плитка с бюджетами -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Будущие и текущие бюджеты -->
<ConfirmDialog/>
<Toast/>
<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>
<div class="flex flex-row justify-between gap-4">
<div class="flex flex-col justify-between gap-5">
<div class="text-xl font-bold ">{{ budget.name }}</div>
<div class="text-sm text-gray-600 ">
{{ formatDate(budget.dateFrom) }} - {{ formatDate(budget.dateTo) }}
</div>
</div>
<div class="flex flex-col justify-between gap-4">
<router-link :to="'/budgets/'+budget.id">
<i class="pi pi-arrow-circle-right text-green-500" style="font-size: 1.5rem;"/>
</router-link>
<button @click="deleteBudget(budget)"><i class="pi pi-trash" style="color:red; font-size: 1.2rem"/></button>
</div>
<div class="text-sm text-gray-600 mb-4">
{{ formatDate(budget.dateFrom) }} - {{ formatDate(budget.dateTo) }}
</div>
<!-- <div class="mb-4">-->
<!-- <div class="mb-4">-->
<!-- <div class="text-sm">Total Income: <span class="font-bold">{{ formatAmount(budgettotalIncomes) }} </span></div>-->
<!-- <div class="text-sm">Total Expenses: <span class="font-bold">{{ formatAmount(budget.totalExpenses) }} </span></div>-->
<!-- <div class="text-sm">Planned Expenses: <span class="font-bold">{{ formatAmount(budget.totalExpenses) }} </span></div>-->
<!-- <div class="text-sm flex items-center">-->
<!-- Unplanned Expenses:-->
<!-- <div class="text-sm flex items-center">-->
<!-- Unplanned Expenses:-->
<!-- <span class="ml-2 font-bold">{{ formatAmount(budget.totalIncomes - budget.totalExpenses) }} </span>-->
<!-- Прогресс бар -->
<!-- <ProgressBar :value="budget.unplannedProgress" class="ml-4 w-full"/>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
</div>
<!-- Прошедшие бюджеты (забеленные) -->
<!-- <div v-for="budget in pastBudgets" :key="budget.id" class="p-4 shadow-lg rounded-lg bg-gray-100 opacity-60">-->
<!-- <div class="text-xl font-bold mb-2">{{ budget.month }}</div>-->
<!-- <div class="text-sm text-gray-600 mb-4">-->
<!-- {{ budget.startDate }} - {{ budget.endDate }}-->
<!-- </div>-->
<!-- <div class="mb-4">-->
<!-- <div class="text-sm">Total Income: <span class="font-bold">{{ budget.totalIncome }}</span></div>-->
<!-- <div class="text-sm">Total Expenses: <span class="font-bold">{{ budget.totalExpenses }}</span></div>-->
<!-- <div class="text-sm">Planned Expenses: <span class="font-bold">{{ budget.plannedExpenses }}</span></div>-->
<!-- <div class="text-sm flex items-center">-->
<!-- Unplanned Expenses:-->
<!-- <span class="ml-2 font-bold">{{ budget.remainingForUnplanned }}</span>-->
<!-- &lt;!&ndash; Прогресс бар &ndash;&gt;-->
<!-- <ProgressBar :value="budget.unplannedProgress" class="ml-4 w-full"/>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!-- <div v-for="budget in pastBudgets" :key="budget.id" class="p-4 shadow-lg rounded-lg bg-gray-100 opacity-60">-->
<!-- <div class="text-xl font-bold mb-2">{{ budget.month }}</div>-->
<!-- <div class="text-sm text-gray-600 mb-4">-->
<!-- {{ budget.startDate }} - {{ budget.endDate }}-->
<!-- </div>-->
<!-- <div class="mb-4">-->
<!-- <div class="text-sm">Total Income: <span class="font-bold">{{ budget.totalIncome }}</span></div>-->
<!-- <div class="text-sm">Total Expenses: <span class="font-bold">{{ budget.totalExpenses }}</span></div>-->
<!-- <div class="text-sm">Planned Expenses: <span class="font-bold">{{ budget.plannedExpenses }}</span></div>-->
<!-- <div class="text-sm flex items-center">-->
<!-- Unplanned Expenses:-->
<!-- <span class="ml-2 font-bold">{{ budget.remainingForUnplanned }}</span>-->
<!-- &lt;!&ndash; Прогресс бар &ndash;&gt;-->
<!-- <ProgressBar :value="budget.unplannedProgress" class="ml-4 w-full"/>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
</div>
</div>
</template>
<script setup lang="ts">
import {onMounted, ref} from 'vue';
import {BudgetInfo} from "@/models/Budget";
import {getBudgetInfos} from "@/services/budgetsService";
import {Budget, BudgetInfo} from "@/models/Budget";
import {createBudget, deleteBudgetRequest, getBudgetInfos} from "@/services/budgetsService";
import {formatDate} from "@/utils/utils";
import LoadingView from "@/components/LoadingView.vue";
import Button from "primevue/button";
import ConfirmDialog from "primevue/confirmdialog";
import BudgetCreationView from "@/components/budgets/BudgetCreationView.vue";
import StatusView from "@/components/StatusView.vue";
import {useConfirm} from "primevue/useconfirm";
import {useToast} from "primevue/usetoast";
import Toast from "primevue/toast";
const confirm = useConfirm();
const toast = useToast();
const loading = ref(false)
const budgetInfos = ref<BudgetInfo[]>([])
const creationOpened = ref(false)
const creationSuccessModal = ref(false)
const creationSuccessShow = async () => {
creationOpened.value = false
const creationSuccessShow = async (budget, createRecurrentPayments) => {
try {
await createBudget(budget, createRecurrentPayments)
budgetInfos.value = await getBudgetInfos()
creationSuccessModal.value = true
setTimeout(() => {
creationSuccessModal.value = false
toast.add({severity: 'success', summary: 'Успешно!', detail: 'Бюджет создан!', life: 3000});
creationOpened.value = false
} catch (error) {
console.log(error.response.data["message"])
toast.add({severity: "error", summary: "Бюджет не создан", detail: error.response.data["message"], life: 3000});
}
, 1000)
// creationSuccessModal.value = true
// setTimeout(() => {
// creationSuccessModal.value = false
// }
// , 1000)
}
const pastBudgets = ref([
{
@@ -106,6 +131,37 @@ const pastBudgets = ref([
},
]);
const deleteBudget = async (budget: Budget) => {
confirm.require({
message: `Вы действительно хотите удалить бюджет ${budget.name} ?`,
header: 'Удаление бюджета',
icon: 'pi pi-info-circle',
rejectLabel: 'Отмена',
rejectProps: {
label: 'Отмена',
severity: 'secondary',
outlined: true
},
acceptProps: {
label: 'Удалить',
severity: 'danger'
},
accept: async () => {
try {
await deleteBudgetRequest(budget.id)
// showToast("Confirmed")
budgetInfos.value = await getBudgetInfos()
toast.add({severity: 'success', summary: 'Успешно!', detail: 'Бюджет удален!', life: 3000});
} catch (e: Error) {
toast.add({severity: 'error', summary: "Ошибка при удалении", detail: e.message, life: 3000});
}
},
reject: () => {
toast.add({severity: 'info', summary: 'Отменено', detail: 'Вы отменили удаление', life: 3000});
}
});
}
onMounted(async () => {

View File

@@ -6,9 +6,10 @@ import {computed, onMounted, PropType, ref} from "vue";
import {Transaction} from "@/models/Transaction";
import {Category, CategoryType} from "@/models/Category";
import {getCategories, getCategoryTypes} from "@/services/categoryService";
import {setTransactionDoneRequest} from "@/services/transactionService";
import { updateTransactionRequest} from "@/services/transactionService";
import {formatAmount, formatDate} from "@/utils/utils";
import TransactionForm from "@/components/transactions/TransactionForm.vue";
import {useToast} from "primevue/usetoast";
const props = defineProps(
@@ -30,31 +31,44 @@ const props = defineProps(
const emits = defineEmits(['open-drawer', 'transaction-checked', 'transaction-updated', 'delete-transaction'])
const setIsDoneTrue = async () => {
setTimeout(async () => {
await setTransactionDoneRequest(props.transaction)
emits('transaction-checked')
}, 10);
console.log("here")
await updateTransactionRequest(props.transaction)
emits('transaction-updated')
}, 20);
// showedTransaction.value.isDone = !showedTransaction.value.isDone;
}
const toast = useToast();
const drawerOpened = ref(false)
const toggleDrawer = () => {
if (props.transaction?.parentId) {
toast.add({
severity: 'warn',
summary: 'Транзакцию нельзя изменить!',
detail: 'Транзакции созданные из плана не могут быть изменены.',
life: 3000
});
} else {
if (drawerOpened.value) {
drawerOpened.value = false;
}
drawerOpened.value = !drawerOpened.value
emits('open-drawer', props.transaction)
}
}
const transactionUpdate = () => {
console.log("transaction updated")
emits('transaction-updated')
}
const isPlanned = computed(() => {
return props.transaction?.transactionType.code === "PLANNED"
return props.transaction?.type.code === "PLANNED"
})
@@ -94,6 +108,8 @@ const closeDrawer = () => {
onMounted(async () => {
// await fetchCategories();
// await fetchCategoryTypes()
})
</script>
@@ -104,12 +120,15 @@ onMounted(async () => {
"
class="flex bg-white min-w-fit max-h-fit flex-row items-center gap-4 w-full ">
<div>
<p v-if="transaction.transactionType.code=='INSTANT' || props.isList"
<p v-if="transaction.type.code=='INSTANT' || props.isList"
class="text-6xl font-bold text-gray-700 dark:text-gray-400">
{{ transaction.category.icon }}</p>
<Checkbox v-model="transaction.isDone" v-else-if="transaction.transactionType.code=='PLANNED' && !props.isList"
{{ transaction.category.icon }}
</p>
<Checkbox v-model="transaction.isDone" v-else-if="transaction.type.code=='PLANNED' && !props.isList"
:binary="true"
@click="setIsDoneTrue"/>
</div>
<button class="flex flex-row items-center p-x-4 justify-between w-full " @click="toggleDrawer">

View File

@@ -15,13 +15,46 @@
/>
</div>
<div :class="!updateLoading ? '' : 'h-fit bg-white opacity-50 z-0 '" class=" flex flex-col gap-3">
<div class="flex flex-col ">
<!-- {{ budget }}-->
<div class="flex flex-row justify-between ">
<div class="flex flex-col gap-2">
<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>
<div class="flex flex-col gap-2 justify-center">
<button class="flex flex-row bg-white py-6 px-4 shadow-lg rounded-lg items-center h-6 justify-center gap-2"
@click="warnsOpened = !warnsOpened">
<span class="bg-gray-300 p-1 rounded font-bold">{{
warns ? warns.length : 0
}}</span><span>Уведомлений</span>
</button>
<div v-if="warnsOpened"
class="absolute h-fit max-h-128 w-128 overflow-auto bg-white shadow-lg rounded-lg top-32 right-4 z-50">
<div v-if="checkWarnsExists" class="flex flex-col p-4">
<div v-for="warn in warns">
<div class="flex flex-row items-center gap-2 justify-between">
<div class="flex flex-row items-center gap-2">
<div class="text-2xl">{{ warn.message.icon }}</div>
<div class="flex flex-col">
<span class="font-bold">{{ warn.message.title }}</span>
<span v-html="warn.message.body"></span>
</div>
</div>
<button @click="hideWarn(warn.id)"><i class="pi pi-times" style="font-size: 0.7rem"></i></button>
</div>
<Divider/>
</div>
</div>
<div v-else class="flex items-center justify-center p-16">
<button @click="fetchWarns(true)">Показать скрытые</button></div>
</div>
</div>
</div>
<div class="flex flex-col gap-2">
<!-- Аналитика и плановые доходы/расходы -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4 items-start ">
@@ -58,6 +91,7 @@
</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) }})
@@ -65,6 +99,8 @@
</div>
</div>
</button>
<div class="grid grid-cols-2 !gap-1 mt-4" :class="detailedShowed ? 'block' : 'hidden'">
<div class="flex flex-col items-center font-bold ">
<p class="font-light ">в первый период</p>
@@ -136,7 +172,7 @@
<div class="flex flex-col items-center ">
<span class="text-sm lg:text-base">Факт. траты 📛</span>
<div class="font-light bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center">
{{ formatAmount(totalInstantExpenses)}}
{{ formatAmount(totalInstantExpenses) }}
</div>
</div>
@@ -144,7 +180,7 @@
<div class="flex flex-col items-center w-full ">
<span class="text-sm lg:text-base">Остаток на траты</span>
<div class="font-light bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center">
{{ formatAmount(totalInstantIncomes - totalInstantExpenses)}}
{{ formatAmount(totalInstantIncomes - totalInstantExpenses) }}
</div>
</div>
<div class="grid grid-cols-2 !gap-1 mt-4" :class="detailedShowed ? 'block' : 'hidden'">
@@ -170,8 +206,10 @@
</div>
<div class=" flex gap-2 overflow-x-auto ">
<button v-for="categorySum in transactionCategoriesSums" @click="selectCategoryType(categorySum.category.id)"
class="rounded-xl border p-1 bg-white border-gray-300 mb-2 min-w-fit px-2" :class="selectedCategoryId == categorySum.category.id ? '!bg-blue-100' : ''">
<button v-for="categorySum in transactionCategoriesSums"
@click="selectCategoryType(categorySum.category.id)"
class="rounded-xl border p-1 bg-white border-gray-300 mb-2 min-w-fit px-2"
:class="selectedCategoryId == categorySum.category.id ? '!bg-blue-100' : ''">
<p><span class="text-sm font-bold">{{ categorySum.category.name }}</span>: {{ categorySum.sum }} </p>
</button>
@@ -181,6 +219,7 @@
:transaction="transaction"
:is-list="true"
@transaction-updated="updateTransactions"
@transaction-checked="updateTransactions"
/>
</div>
</div>
@@ -211,8 +250,9 @@
<ul class="space-y-2">
<!-- {{ plannedIncomes }}-->
<BudgetTransactionView v-for="transaction in plannedIncomes" :transaction="transaction"
:is-list="false" @transaction-checked="fetchBudgetTransactions"
@transaction-updated="updateTransactions"/>
:is-list="false"
@transaction-updated="updateTransactions"
@transaction-checked="updateTransactions"/>
</ul>
@@ -241,7 +281,7 @@
<ul class="space-y-2">
<BudgetTransactionView v-for="transaction in plannedExpenses" :transaction="transaction"
:is-list="false" @transaction-checked="fetchBudgetTransactions"
:is-list="false" @transaction-checked="updateTransactions"
@transaction-updated="updateTransactions"/>
</ul>
</div>
@@ -268,8 +308,10 @@
</button>
</div>
<div class=" flex gap-2 overflow-x-auto">
<button v-for="categorySum in transactionCategoriesSums" @click="selectCategoryType(categorySum.category.id)"
class="rounded-xl border p-1 bg-white border-gray-300 mb-2 min-w-fit px-2" :class="selectedCategoryId == categorySum.category.id ? '!bg-blue-100' : ''">
<button v-for="categorySum in transactionCategoriesSums"
@click="selectCategoryType(categorySum.category.id)"
class="rounded-xl border p-1 bg-white border-gray-300 mb-2 min-w-fit px-2"
:class="selectedCategoryId == categorySum.category.id ? '!bg-blue-100' : ''">
<p><span class="text-sm font-bold">{{ categorySum.category.name }}</span>: {{ categorySum.sum }} </p>
</button>
@@ -280,6 +322,7 @@
:is-list="true"
@transaction-updated="updateTransactions"
/>
</div>
</div>
@@ -288,16 +331,16 @@
</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"
@@ -316,9 +359,11 @@ import {
getBudgetCategories,
getBudgetInfo,
getBudgetTransactions,
updateBudgetCategoryRequest
updateBudgetCategoryRequest,
getWarns,
hideWarnRequest
} from "@/services/budgetsService";
import {Budget, BudgetCategory, BudgetInfo} from "@/models/Budget";
import {Budget, BudgetCategory, Warn} from "@/models/Budget";
import {useRoute} from "vue-router";
import {formatAmount, formatDate} from "@/utils/utils";
import ProgressBar from "primevue/progressbar";
@@ -331,6 +376,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 Divider from "primevue/divider";
import TransactionForm from "@/components/transactions/TransactionForm.vue";
// Зарегистрируем плагин
@@ -350,10 +396,24 @@ const modes = [
const value = ref(50)
const warnsOpened = ref(false);
const hideWarn = async (warnId: string) => {
await hideWarnRequest(route.params.id, warnId)
await fetchWarns()
}
const leftForUnplanned = ref(0)
const budget = ref<Budget>()
const warns = ref<[Warn]>()
const checkWarnsExists = computed(() => {
console.log(warns?.value && warns.value.length > 0 ? "true" : "false");
return warns?.value?.length > 0;
});
const plannedIncomes = ref<Transaction[]>([])
const totalIncomes = computed(() => {
let totalIncome = 0;
@@ -365,7 +425,7 @@ const totalIncomes = computed(() => {
const totalInstantIncomes = computed(() => {
let totalIncome = 0;
transactions.value.filter(t => t.transactionType.code=='INSTANT' && t.category.type.code =='INCOME' ).forEach((i) => {
transactions.value.filter(t => t.type.code == 'INSTANT' && t.category.type.code == 'INCOME').forEach((i) => {
totalIncome += i.amount
})
return totalIncome
@@ -383,7 +443,7 @@ const totalIncomeLeftToGet = computed(() => {
const totalLoans = computed(() => {
let value = 0
categories.value.filter((cat) => cat.category.id == 29).forEach(cat => {
categories.value.filter((cat) => cat.category.id == "675850148198643f121e465d").forEach(cat => {
value += cat.currentLimit
})
return value
@@ -391,18 +451,18 @@ const totalLoans = computed(() => {
const loansRatio = computed(() => {
return totalExpenses.value == 0? 0 : totalLoans.value / totalExpenses.value * 100
return totalExpenses.value == 0 ? 0 : totalLoans.value / totalExpenses.value * 100
})
const savingRatio = computed(() => {
return totalExpenses.value == 0? 0 :totalSaving.value / totalExpenses.value * 100
return totalExpenses.value == 0 ? 0 : totalSaving.value / totalExpenses.value * 100
})
const totalSaving = computed(() => {
let value = 0
categories.value.filter((cat) => cat.category.id == 35).forEach(cat => {
categories.value.filter((cat) => cat.category.id == "675850148198643f121e466a").forEach(cat => {
value += cat.currentLimit
})
return value
@@ -430,7 +490,7 @@ const closeDrawer = async () => {
}
const dailyRatio = computed(() => {
const value = totalExpenses.value == 0? 0 : (totalExpenses.value - totalLoans.value - totalSaving.value) / totalExpenses.value
const value = totalExpenses.value == 0 ? 0 : (totalExpenses.value - totalLoans.value - totalSaving.value) / totalExpenses.value
return value * 100
})
@@ -441,13 +501,13 @@ const plannedExpenses = ref<Transaction[]>([])
const totalExpenses = computed(() => {
let totalExpense = 0;
categories.value.forEach((cat) => {
let catValue = cat.currentLimit - cat.categoryPlannedLimit
plannedExpenses.value.filter(t => t.category.id == cat.category.id).forEach((i) => {
catValue += i.amount
})
let catValue = cat.currentLimit
// plannedExpenses.value.filter(t => t.category.id == cat.category.id).forEach((i) => {
//
// catValue += i.amount
// })
//
totalExpense += catValue
})
@@ -465,7 +525,7 @@ const totalPlannedExpenses = computed(() => {
const totalInstantExpenses = computed(() => {
let totalExpenses = 0;
transactions.value.filter(t => t.transactionType.code=='INSTANT' && t.category.type.code =='EXPENSE').forEach((i) => {
transactions.value.filter(t => t.type.code == 'INSTANT' && t.category.type.code == 'EXPENSE').forEach((i) => {
totalExpenses += i.amount
})
return totalExpenses
@@ -487,14 +547,14 @@ const transactions = ref<Transaction[]>([])
const selectedCategoryId = ref()
const selectCategoryType = (categoryId) => {
if (selectedCategoryId.value==categoryId) {
if (selectedCategoryId.value == categoryId) {
selectedCategoryId.value = null
} else {
selectedCategoryId.value = categoryId
}
}
const filteredTransactions = computed(() => {
return selectedCategoryId.value ? transactions.value.filter(i => i.category.id==selectedCategoryId.value) : transactions.value
return selectedCategoryId.value ? transactions.value.filter(i => i.category.id == selectedCategoryId.value) : transactions.value
})
const fetchBudgetTransactions = async () => {
transactions.value = await getBudgetTransactions(route.params.id, 'INSTANT')
@@ -502,7 +562,12 @@ const fetchBudgetTransactions = async () => {
}
const updateTransactions = async () => {
await Promise.all([fetchPlannedIncomes(), fetchPlannedExpenses(), fetchBudgetCategories(), fetchBudgetTransactions()])
setTimeout(async () => {
await Promise.all([fetchBudgetInfo(),fetchWarns()])
}, 10)
}
const categories = ref<BudgetCategory[]>([])
@@ -541,18 +606,27 @@ const transactionCategoriesSums = computed(() => {
// }
const budgetInfo = ref<BudgetInfo>();
const budgetInfo = ref<Budget>();
const fetchBudgetInfo = async () => {
budget.value = await getBudgetInfo(route.params.id);
plannedExpenses.value = budget.value?.plannedExpenses.copyWithin()
plannedIncomes.value = budget.value?.plannedIncomes.copyWithin()
transactions.value = budget.value?.transactions.copyWithin()
categories.value = budget.value?.categories
updateLoading.value = false
}
const updateBudgetCategory = async (category) => {
// loading.value = true
await updateBudgetCategoryRequest(budget.value.id, category)
category = await updateBudgetCategoryRequest(budget.value.id, category)
await fetchBudgetInfo()
setTimeout(async () => {
await fetchWarns()
}, 500)
// categories.value = await getBudgetCategories(route.params.id)
@@ -590,6 +664,7 @@ const incomesByPeriod = computed(() => {
let incomesUntil25 = 0
let incomesFrom25 = 0
plannedIncomes.value.forEach((i) => {
i.date = new Date(i.date)
if (i.date >= budget.value?.dateFrom && i.date <= twentyFour.value) {
@@ -606,17 +681,19 @@ const incomesByPeriod = computed(() => {
const expensesByPeriod = computed(() => {
let expensesUntil25 = 0
let expensesFrom25 = 0
let totalPlannedExpensesSum = 0
plannedExpenses.value.forEach((i) => {
i.date = new Date(i.date)
if (i.date >= budget.value?.dateFrom && i.date <= twentyFour.value) {
expensesUntil25 += i.amount
} else {
expensesFrom25 += i.amount
}
totalPlannedExpensesSum += i.amount
})
categories.value.forEach((i) => {
expensesUntil25 += (i.currentLimit - i.categoryPlannedLimit) / 2
expensesFrom25 += (i.currentLimit - i.categoryPlannedLimit) / 2
expensesUntil25 += (i.currentLimit - i.currentPlanned) / 2
expensesFrom25 += (i.currentLimit - i.currentPlanned) / 2
})
@@ -760,7 +837,11 @@ const incomeExpenseChartOptions = ref({
}
}
}
});
})
const fetchWarns = async (hidden: Boolean = null) => {
warns.value = await getWarns(route.params.id, hidden)
}
onMounted(async () => {
@@ -768,11 +849,12 @@ onMounted(async () => {
try {
await Promise.all([
fetchBudgetInfo(),
fetchWarns()
// budget.value = await getBudgetInfo(route.params.id),
fetchPlannedIncomes(),
fetchPlannedExpenses(),
fetchBudgetCategories(),
fetchBudgetTransactions(),
// fetchPlannedIncomes(),
// fetchPlannedExpenses(),
// fetchBudgetCategories(),
// fetchBudgetTransactions(),
]);
} catch (error) {
console.error('Error during fetching data:', error);
@@ -795,7 +877,6 @@ onMounted(async () => {
}
.max-h-tlist {
max-height: 1170px; /* Ограничение высоты списка */
}

View File

@@ -39,10 +39,6 @@ const props = defineProps({
categoryType: {
type: String,
required: false
},
transactions: {
type: Array as () => Array<Transaction>,
required: false
}
});
@@ -258,7 +254,7 @@ const closeDrawer = () => emit('close-drawer');
const keyboardOpen = ref(false);
const isMobile = ref(false);
const userAgent = ref(null);
const transactions = ref<Transaction[]>(props.transactions);
const transactions = ref<Transaction[]>(null);
// Мониторинг при монтировании
onMounted(async () => {
loading.value = true;
@@ -267,10 +263,12 @@ onMounted(async () => {
prepareData();
console.log("is editing " + !isEditing.value)
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))
console.log("here blyat")
await getTransactions('INSTANT', 'EXPENSE',null, user.value.id, false, 3 )
.then(transactionsResponse => transactions.value = transactionsResponse.data);
}
loading.value = false;
const deviceInfo = platform;

View File

@@ -36,7 +36,7 @@ onMounted(async () => {
<div v-else class="">
<div class="flex flex-col bg-gray-200 outline outline-2 outline-gray-300 rounded-2xl p-4">
<div class="flex flex-row items-center min-w-fit justify-between">
<p class="text-2xl font-bold">Categories</p>
<p class="text-2xl font-bold">Категории</p>
<router-link to="/settings/categories">
<Button size="large" icon="pi pi-arrow-circle-right" severity="secondary" text rounded/>
</router-link>

View File

@@ -33,7 +33,7 @@ onMounted(async () => {
<div v-else class="">
<div class="flex flex-col bg-gray-200 outline outline-2 outline-gray-300 rounded-2xl p-4">
<div class="flex flex-row items-center min-w-fit justify-between">
<p class="text-2xl font-bold">Recurrent operations</p>
<p class="text-2xl font-bold">Повторяемые операции</p>
<router-link to="/settings/recurrents">
<Button size="large" icon="pi pi-arrow-circle-right" severity="secondary" text rounded/>
</router-link>
@@ -43,7 +43,7 @@ onMounted(async () => {
class="flex rounded-xl border-2 bg-white shadow-xl min-w-fit max-h-fit gap-5 flex-row items-center justify-between w-full p-2">
<div class="flex flex-row items-center p-x-4 gap-4">
<div class="flex flex-row items-center p-x-4 ">
<p class="text-6xl font-bold text-gray-700 dark:text-gray-400">{{ recurrent.category.icon }}</p>
<div class="flex flex-col items-start justify-items-start w-full">
<p class="font-bold">{{ recurrent.name }}</p>

View File

@@ -6,7 +6,7 @@ import RecurrentSettingView from "@/components/settings/RecurrentSettingView.vue
<template>
<div class="flex flex-col h-full px-4 ">
<div class="flex bg-gray-100 flex-col h-full px-4 gap-4 ">
<h2 class="text-4xl font-bold ">Настройки</h2>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 items-start ">

View File

@@ -3,17 +3,18 @@
</div>
<div v-else class="flex flex-col bg-gray-100 dark:bg-gray-800 h-screen p-4">
<ConfirmDialog/>
<!-- Заголовок и кнопка добавления категории -->
<div class="flex flex-row justify-between bg-gray-100">
<h2 class="text-5xl font-bold">Categories</h2>
<Button label="Add Category" icon="pi pi-plus" class="p-button-success" @click="openCreateDialog(null)"/>
<h2 class="text-5xl font-bold">Категории</h2>
<Button label="Добавить категорию" icon="pi pi-plus" @click="openCreateDialog(null)"/>
</div>
<!-- Поле для поиска -->
<div class="my-4 w-full">
<span class="p-input-icon-left flex flex-row gap-2 items-center ">
<i class="pi pi-search"></i>
<InputText v-model="searchTerm" placeholder="Search categories..." class="w-full"/>
<InputText v-model="searchTerm" placeholder="Поиск категорий..." class="w-full"/>
</span>
</div>
@@ -23,6 +24,7 @@
aria-labelledby="category-switch"/>
</div>
<!-- Список категорий с прокруткой для больших экранов -->
<div class="flex">
<div class="hidden sm:grid grid-cols-2 gap-x-10 w-full h-full justify-items-center overflow-y-auto">
@@ -30,7 +32,7 @@
<div class="grid h-full w-full min-w-fit overflow-y-auto">
<div class=" gap-4 ">
<div class="flex flex-row gap-2 ">
<h3 class="text-2xl">Income Categories</h3>
<h3 class="text-2xl">Поступления</h3>
<Button icon="pi pi-plus" rounded outlined class="p-button-success" @click="openCreateDialog('INCOME')"/>
</div>
</div>
@@ -42,7 +44,7 @@
:category="category"
v-bind="category"
@open-edit="openEdit"
@delete-category="deleteCat"
@delete-category="confirmDelete"
/>
</div>
</div>
@@ -51,7 +53,7 @@
<div class="grid h-full w-full min-w-fit overflow-y-auto">
<div class=" gap-4 justify-between ">
<div class="flex flex-row gap-2">
<h3 class="text-2xl">Expense Categories</h3>
<h3 class="text-2xl">Расходы</h3>
<Button icon="pi pi-plus" rounded outlined class="p-button-success !hover:bg-green-600"
@click="openCreateDialog('EXPENSE')"/>
</div>
@@ -63,7 +65,7 @@
:category="category"
v-bind="category"
@open-edit="openEdit"
@delete-category="deleteCat"
@delete-category="confirmDelete"
/>
</div>
</div>
@@ -79,7 +81,7 @@
v-bind="category"
class="mt-2"
@open-edit="openEdit"
@delete-category="deleteCat"
@delete-category="confirmDelete"
/>
</div>
@@ -93,13 +95,15 @@
@update:visible="closeCreateDialog"
/>
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import {computed, nextTick, onMounted, ref, watch} from 'vue';
import Button from 'primevue/button';
import InputText from 'primevue/inputtext';
import SelectButton from 'primevue/selectbutton';
import ConfirmDialog from "primevue/confirmdialog";
import CreateCategoryModal from './CreateCategoryModal.vue';
import CategoryListItem from '@/components/settings/categories/CategoryListItem.vue';
import {Category, CategoryType} from '@/models/Category';
@@ -111,23 +115,20 @@ import {
updateCategory
} from "@/services/categoryService";
export default {
components: {
Button,
InputText,
SelectButton,
CreateCategoryModal,
CategoryListItem,
},
setup() {
const loading = ref(true);
const entireCategories = ref<Category[]>([]);
const expenseCategories = ref<Category[]>([]);
const incomeCategories = ref<Category[]>([]);
const editingCategory = ref<Category | null>(null);
const isDialogVisible = ref(false);
import {useConfirm} from "primevue/useconfirm";
import {useToast} from "primevue/usetoast";
const fetchCategories = async () => {
const loading = ref(true);
const entireCategories = ref<Category[]>([]);
const expenseCategories = ref<Category[]>([]);
const incomeCategories = ref<Category[]>([]);
const editingCategory = ref<Category | null>(null);
const isDialogVisible = ref(false);
const confirm = useConfirm();
const toast = useToast();
const fetchCategories = async () => {
loading.value = true
try {
const response = await getCategories();
@@ -138,11 +139,11 @@ export default {
console.error('Error fetching categories:', error);
}
loading.value = false
};
};
const categoryTypes = ref<CategoryType[]>([]);
const selectedCategoryType = ref<CategoryType | null>(null);
const fetchCategoryTypes = async () => {
const categoryTypes = ref<CategoryType[]>([]);
const selectedCategoryType = ref<CategoryType | null>(null);
const fetchCategoryTypes = async () => {
loading.value = true
try {
const response = await getCategoryTypes();
@@ -152,31 +153,31 @@ export default {
console.error('Error fetching category types:', error);
}
loading.value = false
};
};
const searchTerm = ref('');
const searchTerm = ref('');
const filteredExpenseCategories = computed(() =>
const filteredExpenseCategories = computed(() =>
expenseCategories.value.filter(category =>
category.name.toLowerCase().includes(searchTerm.value.toLowerCase())
)
);
);
const filteredIncomeCategories = computed(() =>
const filteredIncomeCategories = computed(() =>
incomeCategories.value.filter(category =>
category.name.toLowerCase().includes(searchTerm.value.toLowerCase())
)
);
);
const filteredCategories = computed(() => {
const filteredCategories = computed(() => {
if (selectedCategoryType.value?.code === 'EXPENSE') {
return filteredExpenseCategories.value;
} else {
return filteredIncomeCategories.value;
}
});
});
const openCreateDialog = (categoryType: CategoryType | null = null) => {
const openCreateDialog = (categoryType: CategoryType | null = null) => {
if (categoryType) {
selectedCategoryType.value = categoryType;
} else if (editingCategory.value) {
@@ -185,14 +186,14 @@ export default {
selectedCategoryType.value = categoryTypes.value.find(category => category.code === 'EXPENSE');
}
isDialogVisible.value = true;
};
};
const closeCreateDialog = () => {
const closeCreateDialog = () => {
isDialogVisible.value = false;
editingCategory.value = null; // Сбрасываем категорию при закрытии
};
};
const saveCategory = async (newCategory: Category) => {
const saveCategory = async (newCategory: Category) => {
if (newCategory.id) {
await updateCategory(newCategory.id, newCategory);
} else {
@@ -200,52 +201,62 @@ export default {
}
await fetchCategories()
closeCreateDialog();
};
};
const confirmDelete = async (category: Category) => {
confirm.require({
message: 'Вы уверены, что хотите выполнить это действие?\n Это нельзя будет отменить.\nВсе транзакции данной категории будут перенесены в категорию "Другое".\n',
header: `Удаление категории ${category.name}`,
icon: 'pi pi-info-circle',
rejectLabel: 'Cancel',
rejectProps: {
label: 'Cancel',
severity: 'secondary',
outlined: true
},
acceptProps: {
label: 'Delete',
severity: 'danger'
},
accept: async () => {
await deleteCategory(category.id);
await fetchCategories();
toast.add({severity: 'info', summary: 'Confirmed', detail: 'Record deleted', life: 3000});
},
reject: () => {
toast.add({severity: 'error', summary: 'Rejected', detail: 'You have rejected', life: 3000});
}
});
console.log(confirm)
};
const deleteCat = async (categoryId: number) => {
const deleteCat = async (categoryId: number) => {
await deleteCategory(categoryId);
await fetchCategories();
}
}
const openEdit = (category: Category) => {
const openEdit = (category: Category) => {
editingCategory.value = category;
nextTick(() => {
openCreateDialog(category.type); // Обновляем форму для редактирования
});
};
};
watch(editingCategory, (newCategory) => {
watch(editingCategory, (newCategory) => {
if (newCategory) {
selectedCategoryType.value = newCategory.type; // Обновляем тип при редактировании
}
});
});
onMounted(async () => {
onMounted(async () => {
await fetchCategories();
await fetchCategoryTypes();
loading.value = false;
});
});
return {
expenseCategories,
incomeCategories,
selectedCategoryType,
categoryTypes,
searchTerm,
filteredExpenseCategories,
filteredIncomeCategories,
filteredCategories,
isDialogVisible,
openCreateDialog,
closeCreateDialog,
saveCategory,
deleteCat,
openEdit,
editingCategory,
loading,
};
},
};
</script>

View File

@@ -3,6 +3,7 @@ import { Category } from "@/models/Category";
import { PropType } from "vue";
import Button from "primevue/button";
// Определение входных параметров (props)
const props = defineProps({
category: { type: Object as PropType<Category>, required: true }
@@ -19,7 +20,7 @@ const openEdit = () => {
// Функция для удаления категории
const deleteCategory = () => {
console.log('deleteCategory ' + props.category?.id);
emit("delete-category", props.category.id); // Использование события для удаления категории
emit("delete-category", props.category); // Использование события для удаления категории
};
</script>
@@ -33,6 +34,7 @@ const deleteCategory = () => {
<p class="font-light line-clamp-1">{{ category.description }}</p>
</div>
</div>
<div class="flex flex-row items-center p-x-4 gap-2 ">
<Button icon="pi pi-pen-to-square" rounded @click="openEdit"/>
<Button icon="pi pi-trash" severity="danger" rounded @click="deleteCategory"/>

View File

@@ -14,20 +14,20 @@
<!-- SelectButton для выбора типа категории -->
<div class="flex justify-center mt-4">
<SelectButton v-model="categoryType" :options="categoryTypes" optionLabel="name" />
<SelectButton v-if="!isEditing" v-model="categoryType" :options="categoryTypes" optionLabel="name"/>
</div>
<!-- Поля для создания/редактирования категории -->
<label for="newCategoryName">Category Name:</label>
<label for="newCategoryName">Название категории:</label>
<input v-model="name" type="text" id="newCategoryName"/>
<label for="newCategoryDesc">Category Description:</label>
<label for="newCategoryDesc">Описание категории:</label>
<input v-model="description" type="text" id="newCategoryDesc"/>
<!-- Кнопки -->
<div class="button-group">
<button @click="saveCategory" class="create-category-btn">{{ isEditing ? 'Save' : 'Create' }}</button>
<button @click="closeModal" class="close-modal-btn">Cancel</button>
<button @click="saveCategory" class="create-category-btn">{{ isEditing ? 'Сохранить' : 'Создать' }}</button>
<button @click="closeModal" class="close-modal-btn">Отмена</button>
</div>
</Dialog>
</template>
@@ -71,7 +71,7 @@ export default {
}
});
const emojis = ['😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂', '🙂', '🙃', '😉', '😊', '😇', '😍', '🥰', '😘'];
const emojis = ['😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂', '🙂', '🙃', '😉', '😊', '😇', '😍', '🥰', '😘', '🍎', '🍔', '🍕', '🍣', '☕', '🍺', '🚗', '🚕', '🚴‍♂️', '🚆', '✈️', '⛴️', '🛒', '👗', '💍', '👟', '🛍️', '🎮', '🎥', '🎧', '🎢', '🎨', '🏠', '🛏️', '🧹', '🪴', '🍼', '🏥', '💊', '🩺', '🦷', '💳', '💰', '🏦', '🌍', '🗺️', '🏝️', '🏔️', '💻', '📚', '🖋️', '🏫'];
const toggleEmojiPicker = () => {
showEmojiPicker.value = !showEmojiPicker.value;

View File

@@ -23,8 +23,8 @@
<!-- Иконка категории -->
<!-- Информация о платеже -->
<div class="flex flex-row">
<div class="flex items-center gap-4">
<div class="flex flex-row gap-2">
<div class="flex items-center ">
<span class="text-4xl">{{ payment.category.icon }}</span>
</div>
<div class="flex flex-col">

View File

@@ -2,7 +2,7 @@
<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"
<TransactionFormContent :transaction="props.transaction" :transaction-type="transactionType" :category-type="categoryType" @close-drawer="closeDrawer" @create-transaction="transactionUpdated('create')"
@delete-transaction="transactionUpdated" @transaction-updated="transactionUpdated" />
</template>
</DrawerForm>
@@ -55,7 +55,8 @@ const closeDrawer = () => {
emit('close-drawer');
};
const transactionUpdated = () => {
const transactionUpdated = (text) => {
console.log(text)
emit("transaction-updated");
}

View File

@@ -4,7 +4,7 @@ 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 {ref, onMounted, computed, nextTick} from 'vue';
import {Transaction, TransactionType} from "@/models/Transaction";
import {CategoryType} from "@/models/Category";
import SelectButton from "primevue/selectbutton";
@@ -35,10 +35,6 @@ const props = defineProps({
categoryType: {
type: String,
required: false
},
transactions: {
type: Array as () => Array<Transaction>,
required: false
}
});
@@ -71,7 +67,7 @@ const categoryTypes = ref<CategoryType[]>([]);
const transactionTypes = ref<TransactionType[]>([]);
const userStore = useUserStore();
const user = computed( () => userStore.user)
const user = computed(() => userStore.user)
const isReady = computed(() => !loading.value && loadingUser.value)
@@ -103,7 +99,7 @@ const checkForm = () => {
amount: 'Сумма не может быть пустой или 0'
};
if (!editedTransaction.value.transactionType) return showError(errorMessages.transactionType);
if (!editedTransaction.value.type) 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);
@@ -116,7 +112,7 @@ const checkForm = () => {
const prepareData = () => {
if (!props.transaction) {
editedTransaction.value = new Transaction();
editedTransaction.value.transactionType = transactionTypes.value.find(type => type.code === props.transactionType) || transactionTypes.value[0];
editedTransaction.value.type = 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();
@@ -158,20 +154,29 @@ const showError = (message) => {
};
// Создание транзакции
const amountInput = ref(null);
const createTransaction = async () => {
if (checkForm()) {
try {
loading.value = true;
if (editedTransaction.value.transactionType.code === 'INSTANT') {
if (editedTransaction.value.type.code === 'INSTANT') {
editedTransaction.value.isDone = true;
}
await createTransactionRequest(editedTransaction.value);
toast.add({severity: 'success', summary: 'Транзакция создана!', detail: 'Транзакция создана!', life: 3000});
setTimeout(async () => {
amountInput.value.$el.querySelector('input').focus()
}, 10)
emit('create-transaction', editedTransaction.value);
computeResult(true)
toast.add({severity: 'success', summary: 'Успешно!', detail: 'Транзакция создана!', life: 3000});
// computeResult(true)
resetForm();
} catch (error) {
computeResult(false, error)
// computeResult(false, error)
toast.add({severity: 'error', summary: 'Ошибка!', detail: error.response.data["message"], life: 3000});
console.error('Error creating transaction:', error);
} finally {
loading.value = false;
@@ -193,9 +198,9 @@ const updateTransaction = async () => {
// toast.add({severity: 'success', summary: 'Transaction updated!', detail: 'Транзакция обновлена!', life: 3000});
emit('update-transaction', editedTransaction.value);
emit('transaction-updated');
computeResult(true)
toast.add({severity: 'success', summary: 'Успешно!', detail: 'Транзакция создана!', life: 3000});
} catch (error) {
computeResult(false, error)
toast.add({severity: 'error', summary: 'Ошибка!', detail: error.response.data["message"], life: 3000});
console.error('Error updating transaction:', error);
} finally {
loading.value = false;
@@ -216,10 +221,10 @@ const deleteTransaction = async () => {
toast.add({severity: 'success', summary: 'Транзакция удалена!', 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});
toast.add({severity: 'error', summary: 'Ошибка!', detail: error.response.data["message"], life: 3000});
console.error('Error deleting transaction:', error);
} finally {
@@ -238,10 +243,10 @@ const resetForm = () => {
const dateErrorMessage = computed(() => {
if (editedTransaction.value.transactionType.code != 'PLANNED' && editedTransaction.value.date > new Date()) {
if (editedTransaction.value.type.code != 'PLANNED' && editedTransaction.value.date > new Date()) {
return 'При мгновенных тратах дата должна быть меньше текущей!'
} else if (editedTransaction.value.transactionType.code == 'PLANNED' && editedTransaction.value.date < new Date()) {
} else if (editedTransaction.value.type.code == 'PLANNED' && editedTransaction.value.date < new Date()) {
return 'При плановых тратах дата должна быть больше текущей!'
} else {
@@ -255,7 +260,7 @@ const closeDrawer = () => emit('close-drawer');
const keyboardOpen = ref(false);
const isMobile = ref(false);
const userAgent = ref(null);
const transactions = ref<Transaction[]>(props.transactions);
const transactions = ref<Transaction[]>(null);
// Мониторинг при монтировании
onMounted(async () => {
@@ -264,16 +269,18 @@ onMounted(async () => {
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))
console.log("is editing " + !isEditing.value)
if (!isEditing.value) {
console.log("is editing " + !isEditing.value)
await getTransactions('INSTANT', 'EXPENSE', null, user.value.id, false, 3).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';
await nextTick();
console.log('Amount Input Ref:', amountInput.value);
})
</script>
@@ -303,7 +310,7 @@ onMounted(async () => {
<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"
<Select v-if="!isEditing" v-model="editedTransaction.type" :allow-empty="false"
:options="transactionTypes"
optionLabel="name"
aria-labelledby="basic"
@@ -360,6 +367,7 @@ onMounted(async () => {
<FloatLabel variant="on" class="">
<InputNumber class=""
ref="amountInput"
:invalid="!editedTransaction.amount"
:minFractionDigits="0"
id="amount"
@@ -396,7 +404,7 @@ onMounted(async () => {
<DatePicker class="w-full"
inline
:invalid="editedTransaction.transactionType.code != 'PLANNED' ? editedTransaction.date > new Date() : true"
:invalid="editedTransaction.type.code != 'PLANNED' ? editedTransaction.date > new Date() : true"
id="date"
v-model="editedTransaction.date"
dateFormat="yy-mm-dd"

View File

@@ -1,12 +1,15 @@
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import {computed, onMounted, ref} from "vue";
import BudgetTransactionView from "@/components/budgets/BudgetTransactionView.vue";
import IconField from "primevue/iconfield";
import InputIcon from "primevue/inputicon";
import InputText from "primevue/inputtext";
import { getTransactions } from "@/services/transactionService";
import { Transaction } from "@/models/Transaction";
import {getTransactions} from "@/services/transactionService";
import {Transaction} from "@/models/Transaction";
import ProgressSpinner from "primevue/progressspinner";
import {getUsers} from "@/services/userService";
import Button from "primevue/button";
const loading = ref(false);
const searchText = ref("");
@@ -16,12 +19,15 @@ const offset = ref(0); // Начальное смещение
const allLoaded = ref(false); // Флаг для отслеживания окончания данных
// Функция для получения транзакций с параметрами limit и offset
const fetchTransactions = async () => {
if (loading.value || allLoaded.value) return; // Останавливаем загрузку, если уже загружается или данные загружены полностью
const fetchTransactions = async (reload) => {
console.log("here")
console.log(allLoaded.value)
// if (loading.value || allLoaded.value) return; // Останавливаем загрузку, если уже загружается или данные загружены полностью
loading.value = true;
try {
const response = await getTransactions('INSTANT', null,null, null, limit, offset.value);
console.log(reload);
const response = await getTransactions('INSTANT', null, null, selectedUserId.value ? selectedUserId.value : null, null, reload ? offset.value : limit, reload ? 0 : offset.value);
const newTransactions = response.data;
// Проверка на конец данных
@@ -30,7 +36,8 @@ const fetchTransactions = async () => {
}
// Добавляем новые транзакции к текущему списку
transactions.value.push(...newTransactions);
reload ? transactions.value = newTransactions : transactions.value.push(...newTransactions)
offset.value += limit; // Обновляем смещение для следующей загрузки
} catch (error) {
console.error("Error fetching transactions:", error);
@@ -38,6 +45,19 @@ const fetchTransactions = async () => {
loading.value = false;
};
const switchUserFilter = async (user) => {
if (selectedUserId.value == user.id) {
selectedUserId.value = null
} else if (selectedUserId.value == null) {
selectedUserId.value = user.id;
} else {
selectedUserId.value = user.id;
}
await getTransactions('INSTANT', null, null, selectedUserId.value, null, offset.value, 0)
.then(it => transactions.value = it.data)
}
const tgname = computed(() => {
if (window.Telegram.WebApp) {
@@ -48,30 +68,45 @@ const tgname = computed(() => {
// Отфильтрованные транзакции по поисковому запросу
const filteredTransactions = computed(() => {
if (searchText.value.length === 0) {
// Проверяем, есть ли текст поиска
const search = searchText.value.trim().toLowerCase();
if (!search) {
// Если текст поиска пуст, возвращаем все транзакции
return transactions.value;
} else {
return transactions.value.filter(transaction => {
const search = searchText.value.toLowerCase();
return (
transaction.transaction.comment.toLowerCase().includes(search) ||
transaction.transaction.category.name.toLowerCase().includes(search)
);
});
}
// Проверяем наличие данных
if (!transactions.value || !Array.isArray(transactions.value)) {
console.warn("Transactions is not a valid array");
return [];
}
// Фильтруем транзакции по тексту поиска
return transactions.value.filter(transaction => {
return transaction.comment.toLowerCase().includes(search) ||
transaction.category.name.toLowerCase().includes(search);
});
});
// Обработчик прокрутки для ленивой загрузки
const handleScroll = () => {
const bottomReached = window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 2000;
if (bottomReached && !loading.value) {
fetchTransactions(); // Загружаем следующую страницу
}
};
// Обработчик прокрутки для ленивой загрузки
// const handleScroll = () => {
// const bottomReached = window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 2000;
// if (bottomReached && !loading.value) {
// fetchTransactions(); // Загружаем следующую страницу
// }
// };
const users = ref([])
const selectedUserId = ref(null)
const fetchUsers = async () => {
users.value = await getUsers();
}
onMounted(async () => {
await fetchTransactions(); // Первоначальная загрузка данных
window.addEventListener("scroll", handleScroll); // Добавляем обработчик прокрутки
await fetchUsers();
// window.addEventListener("scroll", handleScroll); // Добавляем обработчик прокрутки
});
</script>
@@ -84,20 +119,32 @@ onMounted(async () => {
<InputIcon class="pi pi-search"/>
<InputText v-model="searchText" placeholder="Search"></InputText>
</IconField>
<div class="flex flex-row gap-2">
<!-- <span v-for="user in users">{{user.id}}</span>-->
<button v-for="user in users" @click="switchUserFilter(user)"
class="rounded-xl border p-1 bg-white border-gray-300 mb-2 min-w-fit px-2"
:class="selectedUserId == user.id ? '!bg-blue-100' : ''">
<p><span class="text-sm font-bold">{{ user.firstName }}</span></p>
</button>
</div>
<div class="flex flex-col gap-2">
<BudgetTransactionView
v-for="transaction in filteredTransactions"
:key="transaction.id"
:transaction="transaction"
:is-list="true"
@transaction-updated="fetchTransactions"
@transaction-updated="fetchTransactions(true)"
@delete-transaction="fetchTransactions(true)"
/>
<div class="flex items-center justify-center px-2 py-1 mb-5">
<Button @click="fetchTransactions(false)">Загрузить следующие...</Button>
</div>
<!-- Показать спиннер загрузки, если идет загрузка -->
<ProgressSpinner v-if="loading" class="mb-4" style="width: 50px; height: 50px;"
strokeWidth="8"
fill="transparent"
animationDuration=".5s" />
animationDuration=".5s"/>
</div>
</div>
</div>

View File

@@ -10,11 +10,13 @@ import Ripple from "primevue/ripple";
import ToastService from 'primevue/toastservice'
import Tooltip from 'primevue/tooltip';
import { createPinia } from 'pinia';
import ConfirmationService from 'primevue/confirmationservice';
const app = createApp(App);
app.use(router);
app.use(ToastService);
app.use(ConfirmationService);
app.use(createPinia())
app.directive('ripple', Ripple);
app.directive('tooltip', Tooltip);
@@ -24,6 +26,7 @@ app.use(PrimeVue, {
}
});
app.config.globalProperties.$primevue.config.locale = {
firstDayOfWeek: 1, // Устанавливаем понедельник как первый день недели
dayNames: ["Воскресенье", "Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота"],

View File

@@ -1,20 +1,27 @@
import {Transaction} from "@/models/Transaction";
import {Category, CategorySetting} from "@/models/Category";
import {Category} from "@/models/Category";
export class BudgetInfo {
budget: Budget
export class Budget {
id: number
name: string
dateFrom: Date
dateTo: Date
createdAt: Date
plannedExpenses: [Transaction]
plannedIncomes: [Transaction]
categories: [BudgetCategory]
transactions: [Transaction]
transactionCategoriesSums: []
totalIncomes: number
totalExpenses: number
chartData: [[]]
unplannedCategories: [BudgetCategory]
warns: [Warn]
}
export class Budget {
export class Warn {
id: string
severity: string
message: string
}
export class BudgetInfo {
id: number
name: string
dateFrom: Date
@@ -23,6 +30,8 @@ export class Budget {
}
export class BudgetCategory {
category: Category;
categorySetting: CategorySetting
category: Category
currentSpent: BigDecimal // отображает сумму потраченных на данный момент средств по категории
currentLimit: BigDecimal // отображает текущий лимит по категории
currentPlanned: BigDecimal // отображает текущую сумму запланированных расходов по категории
}

View File

@@ -1,23 +1,24 @@
import {Category} from "@/models/Category";
import {User} from "@/models/User";
export class Transaction {
id: number;
transactionType: TransactionType;
id: String;
type: TransactionType;
user: User
category: Category;
comment: string;
date: Date;
amount: number
parentId: String
isDone: boolean;
}
export class TransactionCategoriesSum{
export class TransactionCategoriesSum {
category: Category;
sum: number
}
export class TransactionType {
code: string;
name: string;

View File

@@ -4,8 +4,8 @@ import { useRouter } from 'vue-router';
// Создаем экземпляр axios
const api = axios.create({
baseURL: 'https://luminic.space/api/v1',
// baseURL: 'http://localhost:8000/api/v1',
baseURL: 'https://luminic.space/api/',
// baseURL: 'http://localhost:8082/api',
});
// Устанавливаем токен из localStorage при каждом запуске
@@ -19,7 +19,7 @@ api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response && error.response.status === 401) {
if (error.response && error.response.status === 403) {
localStorage.removeItem('token');
const router = useRouter();
router.push('/login');

View File

@@ -4,25 +4,32 @@ import {format} from "date-fns";
// Импортируете настроенный экземпляр axios
export const getBudgetInfos = async () => {
try {
let response = await apiClient.get('/budgets/');
let response = await apiClient.get('/budgets');
let budgetInfos = response.data;
budgetInfos.forEach((budgetInfo: Budget) => {
budgetInfo.dateFrom = new Date(budgetInfo.dateFrom);
budgetInfo.dateTo = new Date(budgetInfo.dateTo);
// budgetInfo.plannedExpenses.forEach(e => {
// e.date = new Date(e.date)
// })
//
// budgetInfo.plannedIncomes.forEach(e => {
// e.date = new Date(e.date)
// })
//
// budgetInfo.transactions.forEach(e => {
// e.date = new Date(e.date)
// })
budgetInfo.plannedExpenses?.forEach(e => {
e.date = new Date(e.date)
})
budgetInfo.plannedIncomes?.forEach(e => {
e.date = new Date(e.date)
})
budgetInfo.transactions?.forEach(e => {
e.date = new Date(e.date)
})
})
return budgetInfos
} catch (e) {
console.log(e)
throw e
}
}
export const getBudgetTransactions = async (budgetId, transactionType, categoryType) => {
@@ -66,18 +73,50 @@ export const getBudgetInfo = async (budget_id: number) => {
};
export const getWarns = async (budgetId: string, hidden: Boolean = null) => {
let url = `/budgets/${budgetId}/warns`
if (hidden) {
url += `?hidden=${hidden}`
}
let warns = await apiClient.get(url);
return warns.data
}
export const hideWarnRequest = async (budgetId: string, warnId: string) => {
await apiClient.post(`/budgets/${budgetId}/warns/${warnId}/hide`);
}
export const updateBudgetCategoryRequest = async (budget_id, category: BudgetCategory) => {
await apiClient.put('/budgets/' + budget_id + '/category', category);
return await apiClient.post('/budgets/' + budget_id + '/categories/' + category.category.id + "/limit", {"limit": category.currentLimit}).then(i => i.data);
}
export const createBudget = async (budget: Budget, createRecurrent: Boolean) => {
budget.dateFrom = format(budget.dateFrom, 'yyyy-MM-dd')
budget.dateTo = format(budget.dateTo, 'yyyy-MM-dd')
try {
let budgetToCreate = JSON.parse(JSON.stringify(budget));
budgetToCreate.dateFrom = format(budget.dateFrom, 'yyyy-MM-dd')
budgetToCreate.dateTo = format(budget.dateTo, 'yyyy-MM-dd')
let data = {
budget: budget,
budget: budgetToCreate,
createRecurrent: createRecurrent
}
await apiClient.post('/budgets', data);
budget.dateFrom = format(budget.dateFrom, 'dd.mm.yy')
budget.dateTo = format(budget.dateTo, 'dd.mm.yy')
await apiClient.post('/budgets/', data);
} catch (e){
console.error(e)
throw e
}
}
export const deleteBudgetRequest = async (budgetId: string) => {
try {
// throw Error("test")
let response = await apiClient.delete(`/budgets/${budgetId}`);
} catch (error) {
console.log(error);
throw error;
}
}

View File

@@ -5,7 +5,7 @@ import {Category} from "@/models/Category"; // Импортируете нас
export const getCategories = async (type = null) => {
type = type ? type : ''
return await apiClient.get('/categories/?type=' + type);
return await apiClient.get('/categories?type=' + type);
};
export const getCategoryTypes = async () => {

View File

@@ -7,9 +7,10 @@ export const getTransaction = async (transactionId: int) => {
return await apiClient.post(`/transactions/${transactionId}`,);
}
export const getTransactions = async (transaction_type = null, category_type = null, category_id = null, user_id = null, limit = null, offset = null) => {
export const getTransactions = async (transaction_type = null, category_type = null, category_id = null, user_id = null, is_child = null, limit = null, offset = null) => {
const params = {};
console.log(is_child)
// Add the parameters to the params object if they are not null
if (transaction_type) {
params.transaction_type = transaction_type;
@@ -25,6 +26,9 @@ export const getTransactions = async (transaction_type = null, category_type = n
if (user_id) {
params.user_id = user_id
}
if (is_child!=null){
params.is_child = is_child
}
if (limit) {
params.limit = limit
}
@@ -33,7 +37,7 @@ export const getTransactions = async (transaction_type = null, category_type = n
}
// Use axios to make the GET request, passing the params as the second argument
return await apiClient.get('/transactions/', {
return await apiClient.get('/transactions', {
params: params
});
}
@@ -41,14 +45,17 @@ export const getTransactions = async (transaction_type = null, category_type = n
export const createTransactionRequest = async (transaction: Transaction) => {
transaction.date = format(transaction.date, 'yyyy-MM-dd')
let transactionResponse = await apiClient.post('/transactions', transaction);
console.log(transaction.date)
transaction.date = new Date(transaction.date);
return transactionResponse.data
};
export const updateTransactionRequest = async (transaction: Transaction) => {
const id = transaction.id
transaction.date = format(transaction.date, 'yyyy-MM-dd')
// console.log(transaction.isDone)
// transaction.date = transaction.date.setHours(0,0,0,0)
transaction.date = format(transaction.date, "yyyy-MM-dd")
const response = await apiClient.put(`/transactions/${id}`, transaction);
transaction = response.data
transaction.date = new Date(transaction.date);
@@ -56,15 +63,15 @@ export const updateTransactionRequest = async (transaction: Transaction) => {
return transaction
};
export const setTransactionDoneRequest = async (transaction: Transaction) => {
const id = transaction.id
// transaction.date = format(transaction.date, 'yyyy-MM-dd')
const response = await apiClient.put(`/transactions/${id}/done`, transaction);
transaction = response.data
transaction.date = new Date(transaction.date);
return transaction
};
// export const setTransactionDoneRequest = async (transaction: Transaction) => {
// const id = transaction.id
// // transaction.date = format(transaction.date, 'yyyy-MM-dd')
// const response = await apiClient.patch(`/transactions/${id}/set-done`, transaction);
// // transaction = response.data
// // transaction.date = new Date(transaction.date);
//
// // return transaction
// };
export const deleteTransactionRequest = async (id: number) => {
return await apiClient.delete(`/transactions/${id}`);

View File

@@ -0,0 +1,7 @@
import apiClient from '@/services/axiosSetup';
export const getUsers = async () => {
let users = await apiClient.get('/users/');
return users.data;
}

View File

@@ -1,17 +1,20 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
import {defineStore} from 'pinia';
import {ref} from 'vue';
import apiClient from "@/services/axiosSetup";
import {useRoute, useRouter} from "vue-router";
export const useUserStore = defineStore('user', () => {
const user = ref(null);
const loadingUser = ref(true);
const router = useRouter();
const route = useRoute();
async function fetchUserProfile() {
// Убираем проверку на `loadingUser`, чтобы не блокировать запрос
if (!user.value) {
loadingUser.value = true;
try {
const response = await apiClient.get('/auth/users/me'); // запрос к API для получения данных пользователя
const response = await apiClient.get('/auth/me'); // запрос к API для получения данных пользователя
if (response.status !== 200) throw new Error('Ошибка загрузки данных пользователя');
user.value = response.data;
@@ -24,5 +27,29 @@ export const useUserStore = defineStore('user', () => {
}
}
return { user, loadingUser, fetchUserProfile };
// Основная функция для логина
async function login( username, password, tg_id=null) {
try {
let response;
if (tg_id) {
response = await apiClient.post('/auth/token/tg', {tg_id: tg_id});
} else {
response = await apiClient.post('/auth/login', {
username: username,
password: password,
});
}
const token = response.data.token;
localStorage.setItem('token', token);
apiClient.defaults.headers.common['Authorization'] = `Bearer ${token}`;
await fetchUserProfile();
await router.push(route.query['back'] ? route.query['back'].toString() : '/');
} catch (error) {
console.error(error);
alert('Ошибка входа. Проверьте логин и пароль.');
}
};
return {user, loadingUser, fetchUserProfile, login};
});