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 // public/service-worker.js
self.addEventListener("push", (event) => { self.addEventListener("push", (event) => {
console.log(event)
const data = event.data.json(); const data = event.data.json();
console.log(data); console.log(data);
const options = { const options = {

View File

@@ -2,13 +2,14 @@
<div id="app" class="flex flex-col h-screen bg-gray-300"> <div id="app" class="flex flex-col h-screen bg-gray-300">
<!-- MenuBar всегда фиксирован сверху --> <!-- 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"/> <ToolBar class=" fixed visible lg:invisible bottom-0 z-10"/>
<!-- Контентная часть заполняет оставшееся пространство --> <!-- Контентная часть заполняет оставшееся пространство -->
<div class="flex flex-col flex-grow"> <div class="flex flex-col flex-grow">
<!-- {{ tg_id }}--> <!-- {{ tg_id }}-->
<Button label="Sub" :class="checkNotif ? 'flex' : '!hidden'" @click="checkSubscribe"/> <Button label="Sub" :class="checkNotif ? 'flex' : '!hidden'" @click="checkSubscribe"/>
<router-view /> <router-view />
<div class="bg-gray-100 h-12 block lg:hidden"></div> <div class="bg-gray-100 h-12 block lg:hidden"></div>
</div> </div>
@@ -32,6 +33,7 @@ import {useDrawerStore} from '@/stores/drawerStore'
import TransactionForm from "@/components/transactions/TransactionForm.vue"; import TransactionForm from "@/components/transactions/TransactionForm.vue";
const drawerStore = useDrawerStore(); const drawerStore = useDrawerStore();
const visible = computed(() => drawerStore.visible); const visible = computed(() => drawerStore.visible);
const closeDrawer = () => { const closeDrawer = () => {
@@ -73,7 +75,7 @@ const sendSubscribe = async () => {
console.log("Push subscription:", subscription); console.log("Push subscription:", subscription);
// Отправка подписки на сервер для хранения // Отправка подписки на сервер для хранения
await apiClient.post("/push/subscribe", subscription) await apiClient.post("/subscriptions/subscribe", subscription)
} catch (error) { } catch (error) {
console.error("Failed to subscribe to push:", error); console.error("Failed to subscribe to push:", error);
} }

View File

@@ -17,12 +17,43 @@ ChartJS.register(ChartDataLabels);
const loading = ref(false); const loading = ref(false);
const categoriesSums = ref([]) const categoriesSums = ref([])
const dataTableCategories = ref([])
const fetchCategoriesSums = async () => { const fetchCategoriesSums = async () => {
loading.value = true loading.value = true
try { try {
categoriesSums.value = await getTransactionCategoriesSums() categoriesSums.value = await getTransactionCategoriesSums()
// console.log(categoriesSums.value) // 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) { } catch (error) {
console.error('Error fetching categories sums:', error); console.error('Error fetching categories sums:', error);
} }
@@ -213,8 +244,13 @@ onMounted(async () => {
placeholder="Выберите категории" placeholder="Выберите категории"
:maxSelectedLabels="3" class="w-full md:w-80"/> :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}}--> <!-- {{categories}}-->
<!-- {{// chartData}}--> <!-- {{// chartData}}-->
</div> </div>

View File

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

View File

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

View File

@@ -8,7 +8,6 @@ import DatePicker from "primevue/datepicker";
import {onMounted, ref} from "vue"; import {onMounted, ref} from "vue";
import {getMonthName} from "@/utils/utils"; import {getMonthName} from "@/utils/utils";
import {Budget} from "@/models/Budget"; import {Budget} from "@/models/Budget";
import {createBudget} from "@/services/budgetsService";
const props = defineProps({ const props = defineProps({
opened: { opened: {
@@ -28,10 +27,10 @@ const budget = ref(new Budget())
const create = async () => { const create = async () => {
console.log(budget.value) console.log(budget.value)
try { try {
await createBudget(budget.value, createRecurrentPayments.value) emits("budget-created", budget.value, createRecurrentPayments.value);
emits("budget-created");
} catch (e) { } catch (e) {
console.error(e) console.error(e)
throw e
} }
} }
@@ -40,7 +39,7 @@ const cancel = () => {
emits("close-modal"); emits("close-modal");
} }
onMounted(() => { const resetForm = () => {
budget.value.name = '' budget.value.name = ''
budget.value.dateTo = new Date(); budget.value.dateTo = new Date();
budget.value.dateFrom = new Date(); budget.value.dateFrom = new Date();
@@ -58,8 +57,10 @@ onMounted(() => {
budget.value.dateTo.setMonth(budget.value.dateTo.getMonth() + 2) budget.value.dateTo.setMonth(budget.value.dateTo.getMonth() + 2)
} }
budget.value.name = getMonthName(budget.value.dateFrom.getMonth()) + ' ' + budget.value.dateFrom.getFullYear(); budget.value.name = getMonthName(budget.value.dateFrom.getMonth()) + ' ' + budget.value.dateFrom.getFullYear();
}
onMounted(() => {
resetForm()
}) })
</script> </script>

View File

@@ -5,23 +5,33 @@
<div class="flex flex-row gap-4 items-center"> <div class="flex flex-row gap-4 items-center">
<h2 class="text-4xl font-bold">Бюджеты</h2> <h2 class="text-4xl font-bold">Бюджеты</h2>
<Button label="+ Создать" @click="creationOpened=true" size="small"/> <Button label="+ Создать" @click="creationOpened=true" size="small"/>
<BudgetCreationView :opened="creationOpened" @budget-created="creationSuccessShow()" @close-modal="creationOpened=false" /> <BudgetCreationView :opened="creationOpened" @budget-created="creationSuccessShow"
@close-modal="creationOpened=false"/>
<StatusView :show="creationSuccessModal" :is-error="false" :message="'Бюджет создан!'"/> <StatusView :show="creationSuccessModal" :is-error="false" :message="'Бюджет создан!'"/>
</div> </div>
<!-- Плитка с бюджетами --> <!-- Плитка с бюджетами -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"> <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" <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' : ''"> :class="budget.dateTo < new Date() ? 'bg-gray-100 opacity-60' : ''">
<div class="flex flex-row justify-between"> <div class="flex flex-row justify-between gap-4">
<div class="text-xl font-bold mb-2">{{ budget.name }}</div> <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"> <router-link :to="'/budgets/'+budget.id">
<i class="pi pi-arrow-circle-right text-green-500" style="font-size: 1.5rem;"/> <i class="pi pi-arrow-circle-right text-green-500" style="font-size: 1.5rem;"/>
</router-link> </router-link>
<button @click="deleteBudget(budget)"><i class="pi pi-trash" style="color:red; font-size: 1.2rem"/></button>
</div> </div>
<div class="text-sm text-gray-600 mb-4">
{{ formatDate(budget.dateFrom) }} - {{ formatDate(budget.dateTo) }}
</div> </div>
<!-- <div class="mb-4">--> <!-- <div class="mb-4">-->
<!-- <div class="text-sm">Total Income: <span class="font-bold">{{ formatAmount(budgettotalIncomes) }} </span></div>--> <!-- <div class="text-sm">Total Income: <span class="font-bold">{{ formatAmount(budgettotalIncomes) }} </span></div>-->
<!-- <div class="text-sm">Total Expenses: <span class="font-bold">{{ formatAmount(budget.totalExpenses) }} </span></div>--> <!-- <div class="text-sm">Total Expenses: <span class="font-bold">{{ formatAmount(budget.totalExpenses) }} </span></div>-->
@@ -59,27 +69,42 @@
<script setup lang="ts"> <script setup lang="ts">
import {onMounted, ref} from 'vue'; import {onMounted, ref} from 'vue';
import {BudgetInfo} from "@/models/Budget"; import {Budget, BudgetInfo} from "@/models/Budget";
import {getBudgetInfos} from "@/services/budgetsService"; import {createBudget, deleteBudgetRequest, getBudgetInfos} from "@/services/budgetsService";
import {formatDate} from "@/utils/utils"; import {formatDate} from "@/utils/utils";
import LoadingView from "@/components/LoadingView.vue"; import LoadingView from "@/components/LoadingView.vue";
import Button from "primevue/button"; import Button from "primevue/button";
import ConfirmDialog from "primevue/confirmdialog";
import BudgetCreationView from "@/components/budgets/BudgetCreationView.vue"; import BudgetCreationView from "@/components/budgets/BudgetCreationView.vue";
import StatusView from "@/components/StatusView.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 loading = ref(false)
const budgetInfos = ref<BudgetInfo[]>([]) const budgetInfos = ref<BudgetInfo[]>([])
const creationOpened = ref(false) const creationOpened = ref(false)
const creationSuccessModal = 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() budgetInfos.value = await getBudgetInfos()
creationSuccessModal.value = true toast.add({severity: 'success', summary: 'Успешно!', detail: 'Бюджет создан!', life: 3000});
setTimeout(() => { creationOpened.value = false
creationSuccessModal.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([ 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 () => { onMounted(async () => {

View File

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

View File

@@ -15,13 +15,46 @@
/> />
</div> </div>
<div :class="!updateLoading ? '' : 'h-fit bg-white opacity-50 z-0 '" class=" flex flex-col gap-3"> <div :class="!updateLoading ? '' : 'h-fit bg-white opacity-50 z-0 '" class=" flex flex-col gap-3">
<div class="flex flex-col "> <div class="flex flex-row justify-between ">
<!-- {{ budget }}--> <div class="flex flex-col gap-2">
<h2 class="text-4xl font-bold">Бюджет {{ budget.name }} </h2> <h2 class="text-4xl font-bold">Бюджет {{ budget.name }} </h2>
<div class="flex flex-row gap-2 text-xl">{{ formatDate(budget.dateFrom) }} - <div class="flex flex-row gap-2 text-xl">{{ formatDate(budget.dateFrom) }} -
{{ formatDate(budget.dateTo) }} {{ formatDate(budget.dateTo) }}
</div> </div>
</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="flex flex-col gap-2">
<!-- Аналитика и плановые доходы/расходы --> <!-- Аналитика и плановые доходы/расходы -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4 items-start "> <div class="grid grid-cols-1 lg:grid-cols-3 gap-4 items-start ">
@@ -58,6 +91,7 @@
</div> </div>
<div class="flex flex-col items-center "> <div class="flex flex-col items-center ">
<h4 class="text-lg font-bold ">Расходы</h4> <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" <div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center"
:class="totalExpenses > totalIncomes ? ' text-red-700' : ''"> :class="totalExpenses > totalIncomes ? ' text-red-700' : ''">
-{{ formatAmount(totalExpenses) }} ({{ formatAmount(totalExpenses - totalIncomes) }}) -{{ formatAmount(totalExpenses) }} ({{ formatAmount(totalExpenses - totalIncomes) }})
@@ -65,6 +99,8 @@
</div> </div>
</div> </div>
</button> </button>
<div class="grid grid-cols-2 !gap-1 mt-4" :class="detailedShowed ? 'block' : 'hidden'"> <div class="grid grid-cols-2 !gap-1 mt-4" :class="detailedShowed ? 'block' : 'hidden'">
<div class="flex flex-col items-center font-bold "> <div class="flex flex-col items-center font-bold ">
<p class="font-light ">в первый период</p> <p class="font-light ">в первый период</p>
@@ -170,8 +206,10 @@
</div> </div>
<div class=" flex gap-2 overflow-x-auto "> <div class=" flex gap-2 overflow-x-auto ">
<button v-for="categorySum in transactionCategoriesSums" @click="selectCategoryType(categorySum.category.id)" <button v-for="categorySum in transactionCategoriesSums"
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' : ''"> @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> <p><span class="text-sm font-bold">{{ categorySum.category.name }}</span>: {{ categorySum.sum }} </p>
</button> </button>
@@ -181,6 +219,7 @@
:transaction="transaction" :transaction="transaction"
:is-list="true" :is-list="true"
@transaction-updated="updateTransactions" @transaction-updated="updateTransactions"
@transaction-checked="updateTransactions"
/> />
</div> </div>
</div> </div>
@@ -211,8 +250,9 @@
<ul class="space-y-2"> <ul class="space-y-2">
<!-- {{ plannedIncomes }}--> <!-- {{ plannedIncomes }}-->
<BudgetTransactionView v-for="transaction in plannedIncomes" :transaction="transaction" <BudgetTransactionView v-for="transaction in plannedIncomes" :transaction="transaction"
:is-list="false" @transaction-checked="fetchBudgetTransactions" :is-list="false"
@transaction-updated="updateTransactions"/> @transaction-updated="updateTransactions"
@transaction-checked="updateTransactions"/>
</ul> </ul>
@@ -241,7 +281,7 @@
<ul class="space-y-2"> <ul class="space-y-2">
<BudgetTransactionView v-for="transaction in plannedExpenses" :transaction="transaction" <BudgetTransactionView v-for="transaction in plannedExpenses" :transaction="transaction"
:is-list="false" @transaction-checked="fetchBudgetTransactions" :is-list="false" @transaction-checked="updateTransactions"
@transaction-updated="updateTransactions"/> @transaction-updated="updateTransactions"/>
</ul> </ul>
</div> </div>
@@ -268,8 +308,10 @@
</button> </button>
</div> </div>
<div class=" flex gap-2 overflow-x-auto"> <div class=" flex gap-2 overflow-x-auto">
<button v-for="categorySum in transactionCategoriesSums" @click="selectCategoryType(categorySum.category.id)" <button v-for="categorySum in transactionCategoriesSums"
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' : ''"> @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> <p><span class="text-sm font-bold">{{ categorySum.category.name }}</span>: {{ categorySum.sum }} </p>
</button> </button>
@@ -280,6 +322,7 @@
:is-list="true" :is-list="true"
@transaction-updated="updateTransactions" @transaction-updated="updateTransactions"
/> />
</div> </div>
</div> </div>
@@ -316,9 +359,11 @@ import {
getBudgetCategories, getBudgetCategories,
getBudgetInfo, getBudgetInfo,
getBudgetTransactions, getBudgetTransactions,
updateBudgetCategoryRequest updateBudgetCategoryRequest,
getWarns,
hideWarnRequest
} from "@/services/budgetsService"; } from "@/services/budgetsService";
import {Budget, BudgetCategory, BudgetInfo} from "@/models/Budget"; import {Budget, BudgetCategory, Warn} from "@/models/Budget";
import {useRoute} from "vue-router"; import {useRoute} from "vue-router";
import {formatAmount, formatDate} from "@/utils/utils"; import {formatAmount, formatDate} from "@/utils/utils";
import ProgressBar from "primevue/progressbar"; import ProgressBar from "primevue/progressbar";
@@ -331,6 +376,7 @@ import LoadingView from "@/components/LoadingView.vue";
import ChartDataLabels from 'chartjs-plugin-datalabels'; import ChartDataLabels from 'chartjs-plugin-datalabels';
import {Chart as ChartJS} from 'chart.js/auto'; import {Chart as ChartJS} from 'chart.js/auto';
import SelectButton from "primevue/selectbutton"; import SelectButton from "primevue/selectbutton";
import Divider from "primevue/divider";
import TransactionForm from "@/components/transactions/TransactionForm.vue"; import TransactionForm from "@/components/transactions/TransactionForm.vue";
// Зарегистрируем плагин // Зарегистрируем плагин
@@ -350,10 +396,24 @@ const modes = [
const value = ref(50) 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 leftForUnplanned = ref(0)
const budget = ref<Budget>() 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 plannedIncomes = ref<Transaction[]>([])
const totalIncomes = computed(() => { const totalIncomes = computed(() => {
let totalIncome = 0; let totalIncome = 0;
@@ -365,7 +425,7 @@ const totalIncomes = computed(() => {
const totalInstantIncomes = computed(() => { const totalInstantIncomes = computed(() => {
let totalIncome = 0; 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 totalIncome += i.amount
}) })
return totalIncome return totalIncome
@@ -383,7 +443,7 @@ const totalIncomeLeftToGet = computed(() => {
const totalLoans = computed(() => { const totalLoans = computed(() => {
let value = 0 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 value += cat.currentLimit
}) })
return value return value
@@ -402,7 +462,7 @@ const savingRatio = computed(() => {
const totalSaving = computed(() => { const totalSaving = computed(() => {
let value = 0 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 value += cat.currentLimit
}) })
return value return value
@@ -441,13 +501,13 @@ const plannedExpenses = ref<Transaction[]>([])
const totalExpenses = computed(() => { const totalExpenses = computed(() => {
let totalExpense = 0; let totalExpense = 0;
categories.value.forEach((cat) => { categories.value.forEach((cat) => {
let catValue = cat.currentLimit - cat.categoryPlannedLimit let catValue = cat.currentLimit
plannedExpenses.value.filter(t => t.category.id == cat.category.id).forEach((i) => {
catValue += i.amount
})
// plannedExpenses.value.filter(t => t.category.id == cat.category.id).forEach((i) => {
//
// catValue += i.amount
// })
//
totalExpense += catValue totalExpense += catValue
}) })
@@ -465,7 +525,7 @@ const totalPlannedExpenses = computed(() => {
const totalInstantExpenses = computed(() => { const totalInstantExpenses = computed(() => {
let totalExpenses = 0; 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 totalExpenses += i.amount
}) })
return totalExpenses return totalExpenses
@@ -502,7 +562,12 @@ const fetchBudgetTransactions = async () => {
} }
const updateTransactions = async () => { const updateTransactions = async () => {
await Promise.all([fetchPlannedIncomes(), fetchPlannedExpenses(), fetchBudgetCategories(), fetchBudgetTransactions()])
setTimeout(async () => {
await Promise.all([fetchBudgetInfo(),fetchWarns()])
}, 10)
} }
const categories = ref<BudgetCategory[]>([]) const categories = ref<BudgetCategory[]>([])
@@ -541,18 +606,27 @@ const transactionCategoriesSums = computed(() => {
// } // }
const budgetInfo = ref<BudgetInfo>(); const budgetInfo = ref<Budget>();
const fetchBudgetInfo = async () => { const fetchBudgetInfo = async () => {
budget.value = await getBudgetInfo(route.params.id); 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 updateLoading.value = false
} }
const updateBudgetCategory = async (category) => { const updateBudgetCategory = async (category) => {
// loading.value = true // 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) // categories.value = await getBudgetCategories(route.params.id)
@@ -590,6 +664,7 @@ const incomesByPeriod = computed(() => {
let incomesUntil25 = 0 let incomesUntil25 = 0
let incomesFrom25 = 0 let incomesFrom25 = 0
plannedIncomes.value.forEach((i) => { plannedIncomes.value.forEach((i) => {
i.date = new Date(i.date)
if (i.date >= budget.value?.dateFrom && i.date <= twentyFour.value) { if (i.date >= budget.value?.dateFrom && i.date <= twentyFour.value) {
@@ -606,17 +681,19 @@ const incomesByPeriod = computed(() => {
const expensesByPeriod = computed(() => { const expensesByPeriod = computed(() => {
let expensesUntil25 = 0 let expensesUntil25 = 0
let expensesFrom25 = 0 let expensesFrom25 = 0
let totalPlannedExpensesSum = 0
plannedExpenses.value.forEach((i) => { plannedExpenses.value.forEach((i) => {
i.date = new Date(i.date)
if (i.date >= budget.value?.dateFrom && i.date <= twentyFour.value) { if (i.date >= budget.value?.dateFrom && i.date <= twentyFour.value) {
expensesUntil25 += i.amount expensesUntil25 += i.amount
} else { } else {
expensesFrom25 += i.amount expensesFrom25 += i.amount
} }
totalPlannedExpensesSum += i.amount
}) })
categories.value.forEach((i) => { categories.value.forEach((i) => {
expensesUntil25 += (i.currentLimit - i.categoryPlannedLimit) / 2 expensesUntil25 += (i.currentLimit - i.currentPlanned) / 2
expensesFrom25 += (i.currentLimit - i.categoryPlannedLimit) / 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 () => { onMounted(async () => {
@@ -768,11 +849,12 @@ onMounted(async () => {
try { try {
await Promise.all([ await Promise.all([
fetchBudgetInfo(), fetchBudgetInfo(),
fetchWarns()
// budget.value = await getBudgetInfo(route.params.id), // budget.value = await getBudgetInfo(route.params.id),
fetchPlannedIncomes(), // fetchPlannedIncomes(),
fetchPlannedExpenses(), // fetchPlannedExpenses(),
fetchBudgetCategories(), // fetchBudgetCategories(),
fetchBudgetTransactions(), // fetchBudgetTransactions(),
]); ]);
} catch (error) { } catch (error) {
console.error('Error during fetching data:', error); console.error('Error during fetching data:', error);
@@ -795,7 +877,6 @@ onMounted(async () => {
} }
.max-h-tlist { .max-h-tlist {
max-height: 1170px; /* Ограничение высоты списка */ max-height: 1170px; /* Ограничение высоты списка */
} }

View File

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

View File

@@ -36,7 +36,7 @@ onMounted(async () => {
<div v-else class=""> <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-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"> <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"> <router-link to="/settings/categories">
<Button size="large" icon="pi pi-arrow-circle-right" severity="secondary" text rounded/> <Button size="large" icon="pi pi-arrow-circle-right" severity="secondary" text rounded/>
</router-link> </router-link>

View File

@@ -33,7 +33,7 @@ onMounted(async () => {
<div v-else class=""> <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-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"> <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"> <router-link to="/settings/recurrents">
<Button size="large" icon="pi pi-arrow-circle-right" severity="secondary" text rounded/> <Button size="large" icon="pi pi-arrow-circle-right" severity="secondary" text rounded/>
</router-link> </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"> 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> <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"> <div class="flex flex-col items-start justify-items-start w-full">
<p class="font-bold">{{ recurrent.name }}</p> <p class="font-bold">{{ recurrent.name }}</p>

View File

@@ -6,7 +6,7 @@ import RecurrentSettingView from "@/components/settings/RecurrentSettingView.vue
<template> <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> <h2 class="text-4xl font-bold ">Настройки</h2>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 items-start "> <div class="grid grid-cols-1 lg:grid-cols-2 gap-4 items-start ">

View File

@@ -3,17 +3,18 @@
</div> </div>
<div v-else class="flex flex-col bg-gray-100 dark:bg-gray-800 h-screen p-4"> <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"> <div class="flex flex-row justify-between bg-gray-100">
<h2 class="text-5xl font-bold">Categories</h2> <h2 class="text-5xl font-bold">Категории</h2>
<Button label="Add Category" icon="pi pi-plus" class="p-button-success" @click="openCreateDialog(null)"/> <Button label="Добавить категорию" icon="pi pi-plus" @click="openCreateDialog(null)"/>
</div> </div>
<!-- Поле для поиска --> <!-- Поле для поиска -->
<div class="my-4 w-full"> <div class="my-4 w-full">
<span class="p-input-icon-left flex flex-row gap-2 items-center "> <span class="p-input-icon-left flex flex-row gap-2 items-center ">
<i class="pi pi-search"></i> <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> </span>
</div> </div>
@@ -23,6 +24,7 @@
aria-labelledby="category-switch"/> aria-labelledby="category-switch"/>
</div> </div>
<!-- Список категорий с прокруткой для больших экранов --> <!-- Список категорий с прокруткой для больших экранов -->
<div class="flex"> <div class="flex">
<div class="hidden sm:grid grid-cols-2 gap-x-10 w-full h-full justify-items-center overflow-y-auto"> <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="grid h-full w-full min-w-fit overflow-y-auto">
<div class=" gap-4 "> <div class=" gap-4 ">
<div class="flex flex-row gap-2 "> <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')"/> <Button icon="pi pi-plus" rounded outlined class="p-button-success" @click="openCreateDialog('INCOME')"/>
</div> </div>
</div> </div>
@@ -42,7 +44,7 @@
:category="category" :category="category"
v-bind="category" v-bind="category"
@open-edit="openEdit" @open-edit="openEdit"
@delete-category="deleteCat" @delete-category="confirmDelete"
/> />
</div> </div>
</div> </div>
@@ -51,7 +53,7 @@
<div class="grid h-full w-full min-w-fit overflow-y-auto"> <div class="grid h-full w-full min-w-fit overflow-y-auto">
<div class=" gap-4 justify-between "> <div class=" gap-4 justify-between ">
<div class="flex flex-row gap-2"> <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" <Button icon="pi pi-plus" rounded outlined class="p-button-success !hover:bg-green-600"
@click="openCreateDialog('EXPENSE')"/> @click="openCreateDialog('EXPENSE')"/>
</div> </div>
@@ -63,7 +65,7 @@
:category="category" :category="category"
v-bind="category" v-bind="category"
@open-edit="openEdit" @open-edit="openEdit"
@delete-category="deleteCat" @delete-category="confirmDelete"
/> />
</div> </div>
</div> </div>
@@ -79,7 +81,7 @@
v-bind="category" v-bind="category"
class="mt-2" class="mt-2"
@open-edit="openEdit" @open-edit="openEdit"
@delete-category="deleteCat" @delete-category="confirmDelete"
/> />
</div> </div>
@@ -93,13 +95,15 @@
@update:visible="closeCreateDialog" @update:visible="closeCreateDialog"
/> />
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import {computed, nextTick, onMounted, ref, watch} from 'vue'; import {computed, nextTick, onMounted, ref, watch} from 'vue';
import Button from 'primevue/button'; import Button from 'primevue/button';
import InputText from 'primevue/inputtext'; import InputText from 'primevue/inputtext';
import SelectButton from 'primevue/selectbutton'; import SelectButton from 'primevue/selectbutton';
import ConfirmDialog from "primevue/confirmdialog";
import CreateCategoryModal from './CreateCategoryModal.vue'; import CreateCategoryModal from './CreateCategoryModal.vue';
import CategoryListItem from '@/components/settings/categories/CategoryListItem.vue'; import CategoryListItem from '@/components/settings/categories/CategoryListItem.vue';
import {Category, CategoryType} from '@/models/Category'; import {Category, CategoryType} from '@/models/Category';
@@ -111,21 +115,18 @@ import {
updateCategory updateCategory
} from "@/services/categoryService"; } from "@/services/categoryService";
export default { import {useConfirm} from "primevue/useconfirm";
components: { import {useToast} from "primevue/usetoast";
Button,
InputText,
SelectButton,
CreateCategoryModal,
CategoryListItem,
},
setup() {
const loading = ref(true); const loading = ref(true);
const entireCategories = ref<Category[]>([]); const entireCategories = ref<Category[]>([]);
const expenseCategories = ref<Category[]>([]); const expenseCategories = ref<Category[]>([]);
const incomeCategories = ref<Category[]>([]); const incomeCategories = ref<Category[]>([]);
const editingCategory = ref<Category | null>(null); const editingCategory = ref<Category | null>(null);
const isDialogVisible = ref(false); const isDialogVisible = ref(false);
const confirm = useConfirm();
const toast = useToast();
const fetchCategories = async () => { const fetchCategories = async () => {
loading.value = true loading.value = true
@@ -202,7 +203,36 @@ export default {
closeCreateDialog(); 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 deleteCategory(categoryId);
await fetchCategories(); await fetchCategories();
} }
@@ -226,26 +256,7 @@ export default {
loading.value = false; loading.value = false;
}); });
return {
expenseCategories,
incomeCategories,
selectedCategoryType,
categoryTypes,
searchTerm,
filteredExpenseCategories,
filteredIncomeCategories,
filteredCategories,
isDialogVisible,
openCreateDialog,
closeCreateDialog,
saveCategory,
deleteCat,
openEdit,
editingCategory,
loading,
};
},
};
</script> </script>

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
<div class="card flex justify-center h-fit"> <div class="card flex justify-center h-fit">
<DrawerForm v-if="isDesktop" :visible="visible" :isEditing="isEditing" @close-drawer="closeDrawer" > <DrawerForm v-if="isDesktop" :visible="visible" :isEditing="isEditing" @close-drawer="closeDrawer" >
<template #default> <template #default>
<TransactionFormContent :transaction="props.transaction" :transaction-type="transactionType" :category-type="categoryType" @close-drawer="closeDrawer" @create-transaction="transactionUpdated" <TransactionFormContent :transaction="props.transaction" :transaction-type="transactionType" :category-type="categoryType" @close-drawer="closeDrawer" @create-transaction="transactionUpdated('create')"
@delete-transaction="transactionUpdated" @transaction-updated="transactionUpdated" /> @delete-transaction="transactionUpdated" @transaction-updated="transactionUpdated" />
</template> </template>
</DrawerForm> </DrawerForm>
@@ -55,7 +55,8 @@ const closeDrawer = () => {
emit('close-drawer'); emit('close-drawer');
}; };
const transactionUpdated = () => { const transactionUpdated = (text) => {
console.log(text)
emit("transaction-updated"); emit("transaction-updated");
} }

View File

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

View File

@@ -4,9 +4,12 @@ import BudgetTransactionView from "@/components/budgets/BudgetTransactionView.vu
import IconField from "primevue/iconfield"; import IconField from "primevue/iconfield";
import InputIcon from "primevue/inputicon"; import InputIcon from "primevue/inputicon";
import InputText from "primevue/inputtext"; import InputText from "primevue/inputtext";
import {getTransactions} from "@/services/transactionService"; import {getTransactions} from "@/services/transactionService";
import {Transaction} from "@/models/Transaction"; import {Transaction} from "@/models/Transaction";
import ProgressSpinner from "primevue/progressspinner"; import ProgressSpinner from "primevue/progressspinner";
import {getUsers} from "@/services/userService";
import Button from "primevue/button";
const loading = ref(false); const loading = ref(false);
const searchText = ref(""); const searchText = ref("");
@@ -16,12 +19,15 @@ const offset = ref(0); // Начальное смещение
const allLoaded = ref(false); // Флаг для отслеживания окончания данных const allLoaded = ref(false); // Флаг для отслеживания окончания данных
// Функция для получения транзакций с параметрами limit и offset // Функция для получения транзакций с параметрами limit и offset
const fetchTransactions = async () => { const fetchTransactions = async (reload) => {
if (loading.value || allLoaded.value) return; // Останавливаем загрузку, если уже загружается или данные загружены полностью console.log("here")
console.log(allLoaded.value)
// if (loading.value || allLoaded.value) return; // Останавливаем загрузку, если уже загружается или данные загружены полностью
loading.value = true; loading.value = true;
try { 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; 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; // Обновляем смещение для следующей загрузки offset.value += limit; // Обновляем смещение для следующей загрузки
} catch (error) { } catch (error) {
console.error("Error fetching transactions:", error); console.error("Error fetching transactions:", error);
@@ -38,6 +45,19 @@ const fetchTransactions = async () => {
loading.value = false; 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(() => { const tgname = computed(() => {
if (window.Telegram.WebApp) { if (window.Telegram.WebApp) {
@@ -48,30 +68,45 @@ const tgname = computed(() => {
// Отфильтрованные транзакции по поисковому запросу // Отфильтрованные транзакции по поисковому запросу
const filteredTransactions = computed(() => { const filteredTransactions = computed(() => {
if (searchText.value.length === 0) { // Проверяем, есть ли текст поиска
const search = searchText.value.trim().toLowerCase();
if (!search) {
// Если текст поиска пуст, возвращаем все транзакции
return transactions.value; 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 handleScroll = () => {
const bottomReached = window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 2000; // const bottomReached = window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 2000;
if (bottomReached && !loading.value) { // if (bottomReached && !loading.value) {
fetchTransactions(); // Загружаем следующую страницу // fetchTransactions(); // Загружаем следующую страницу
} // }
}; // };
const users = ref([])
const selectedUserId = ref(null)
const fetchUsers = async () => {
users.value = await getUsers();
}
onMounted(async () => { onMounted(async () => {
await fetchTransactions(); // Первоначальная загрузка данных await fetchTransactions(); // Первоначальная загрузка данных
window.addEventListener("scroll", handleScroll); // Добавляем обработчик прокрутки await fetchUsers();
// window.addEventListener("scroll", handleScroll); // Добавляем обработчик прокрутки
}); });
</script> </script>
@@ -84,15 +119,27 @@ onMounted(async () => {
<InputIcon class="pi pi-search"/> <InputIcon class="pi pi-search"/>
<InputText v-model="searchText" placeholder="Search"></InputText> <InputText v-model="searchText" placeholder="Search"></InputText>
</IconField> </IconField>
<div class="flex flex-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"> <div class="flex flex-col gap-2">
<BudgetTransactionView <BudgetTransactionView
v-for="transaction in filteredTransactions" v-for="transaction in filteredTransactions"
:key="transaction.id" :key="transaction.id"
:transaction="transaction" :transaction="transaction"
:is-list="true" :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;" <ProgressSpinner v-if="loading" class="mb-4" style="width: 50px; height: 50px;"
strokeWidth="8" strokeWidth="8"

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,25 +4,32 @@ import {format} from "date-fns";
// Импортируете настроенный экземпляр axios // Импортируете настроенный экземпляр axios
export const getBudgetInfos = async () => { export const getBudgetInfos = async () => {
try {
let response = await apiClient.get('/budgets/');
let response = await apiClient.get('/budgets');
let budgetInfos = response.data; let budgetInfos = response.data;
budgetInfos.forEach((budgetInfo: Budget) => { budgetInfos.forEach((budgetInfo: Budget) => {
budgetInfo.dateFrom = new Date(budgetInfo.dateFrom); budgetInfo.dateFrom = new Date(budgetInfo.dateFrom);
budgetInfo.dateTo = new Date(budgetInfo.dateTo); budgetInfo.dateTo = new Date(budgetInfo.dateTo);
// budgetInfo.plannedExpenses.forEach(e => { budgetInfo.plannedExpenses?.forEach(e => {
// e.date = new Date(e.date) e.date = new Date(e.date)
// }) })
//
// budgetInfo.plannedIncomes.forEach(e => { budgetInfo.plannedIncomes?.forEach(e => {
// e.date = new Date(e.date) e.date = new Date(e.date)
// }) })
//
// budgetInfo.transactions.forEach(e => { budgetInfo.transactions?.forEach(e => {
// e.date = new Date(e.date) e.date = new Date(e.date)
// }) })
}) })
return budgetInfos return budgetInfos
} catch (e) {
console.log(e)
throw e
}
} }
export const getBudgetTransactions = async (budgetId, transactionType, categoryType) => { 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) => { 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) => { 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 = { let data = {
budget: budget, budget: budgetToCreate,
createRecurrent: createRecurrent createRecurrent: createRecurrent
} }
await apiClient.post('/budgets', data); await apiClient.post('/budgets/', data);
budget.dateFrom = format(budget.dateFrom, 'dd.mm.yy')
budget.dateTo = format(budget.dateTo, 'dd.mm.yy') } 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) => { export const getCategories = async (type = null) => {
type = type ? type : '' type = type ? type : ''
return await apiClient.get('/categories/?type=' + type); return await apiClient.get('/categories?type=' + type);
}; };
export const getCategoryTypes = async () => { export const getCategoryTypes = async () => {

View File

@@ -7,9 +7,10 @@ export const getTransaction = async (transactionId: int) => {
return await apiClient.post(`/transactions/${transactionId}`,); return await apiClient.post(`/transactions/${transactionId}`,);
} }
export const getTransactions = async (transaction_type = null, category_type = null, category_id = null, user_id = null, 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 = {}; const params = {};
console.log(is_child)
// Add the parameters to the params object if they are not null // Add the parameters to the params object if they are not null
if (transaction_type) { if (transaction_type) {
params.transaction_type = transaction_type; params.transaction_type = transaction_type;
@@ -25,6 +26,9 @@ export const getTransactions = async (transaction_type = null, category_type = n
if (user_id) { if (user_id) {
params.user_id = user_id params.user_id = user_id
} }
if (is_child!=null){
params.is_child = is_child
}
if (limit) { if (limit) {
params.limit = 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 // 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 params: params
}); });
} }
@@ -41,14 +45,17 @@ export const getTransactions = async (transaction_type = null, category_type = n
export const createTransactionRequest = async (transaction: Transaction) => { export const createTransactionRequest = async (transaction: Transaction) => {
transaction.date = format(transaction.date, 'yyyy-MM-dd') transaction.date = format(transaction.date, 'yyyy-MM-dd')
let transactionResponse = await apiClient.post('/transactions', transaction); let transactionResponse = await apiClient.post('/transactions', transaction);
console.log(transaction.date)
transaction.date = new Date(transaction.date); transaction.date = new Date(transaction.date);
return transactionResponse.data return transactionResponse.data
}; };
export const updateTransactionRequest = async (transaction: Transaction) => { export const updateTransactionRequest = async (transaction: Transaction) => {
const id = transaction.id 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); const response = await apiClient.put(`/transactions/${id}`, transaction);
transaction = response.data transaction = response.data
transaction.date = new Date(transaction.date); transaction.date = new Date(transaction.date);
@@ -56,15 +63,15 @@ export const updateTransactionRequest = async (transaction: Transaction) => {
return transaction return transaction
}; };
export const setTransactionDoneRequest = async (transaction: Transaction) => { // export const setTransactionDoneRequest = async (transaction: Transaction) => {
const id = transaction.id // const id = transaction.id
// transaction.date = format(transaction.date, 'yyyy-MM-dd') // // transaction.date = format(transaction.date, 'yyyy-MM-dd')
const response = await apiClient.put(`/transactions/${id}/done`, transaction); // const response = await apiClient.patch(`/transactions/${id}/set-done`, transaction);
transaction = response.data // // transaction = response.data
transaction.date = new Date(transaction.date); // // transaction.date = new Date(transaction.date);
//
return transaction // // return transaction
}; // };
export const deleteTransactionRequest = async (id: number) => { export const deleteTransactionRequest = async (id: number) => {
return await apiClient.delete(`/transactions/${id}`); 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 {defineStore} from 'pinia';
import {ref} from 'vue'; import {ref} from 'vue';
import apiClient from "@/services/axiosSetup"; import apiClient from "@/services/axiosSetup";
import {useRoute, useRouter} from "vue-router";
export const useUserStore = defineStore('user', () => { export const useUserStore = defineStore('user', () => {
const user = ref(null); const user = ref(null);
const loadingUser = ref(true); const loadingUser = ref(true);
const router = useRouter();
const route = useRoute();
async function fetchUserProfile() { async function fetchUserProfile() {
// Убираем проверку на `loadingUser`, чтобы не блокировать запрос // Убираем проверку на `loadingUser`, чтобы не блокировать запрос
if (!user.value) { if (!user.value) {
loadingUser.value = true; loadingUser.value = true;
try { 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('Ошибка загрузки данных пользователя'); if (response.status !== 200) throw new Error('Ошибка загрузки данных пользователя');
user.value = response.data; 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};
}); });