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);
} }
@@ -37,8 +68,8 @@ const fetchCategories = async () => {
try { try {
const response = await getCategories('EXPENSE'); const response = await getCategories('EXPENSE');
categories.value = response.data categories.value = response.data
console.log(categories.value.filter(i => i.id==30)) console.log(categories.value.filter(i => i.id == 30))
selectedCategories.value.push(categories.value.filter(i => i.id==30)[0]) selectedCategories.value.push(categories.value.filter(i => i.id == 30)[0])
} catch (error) { } catch (error) {
console.error('Error fetching categories:', error); console.error('Error fetching categories:', error);
} }
@@ -170,9 +201,9 @@ const setChartOptions = () => {
offset: 8, // Отступ от точки offset: 8, // Отступ от точки
labels: { labels: {
font: { font: {
weight: 'bold' weight: 'bold'
} }
} }
} }
}, },
@@ -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,81 +5,106 @@
<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">
<router-link :to="'/budgets/'+budget.id"> <div class="text-xl font-bold ">{{ budget.name }}</div>
<i class="pi pi-arrow-circle-right text-green-500" style="font-size: 1.5rem;"/> <div class="text-sm text-gray-600 ">
</router-link> {{ 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> </div>
<div class="text-sm text-gray-600 mb-4">
{{ formatDate(budget.dateFrom) }} - {{ formatDate(budget.dateTo) }} <!-- <div class="mb-4">-->
</div> <!-- <div class="text-sm">Total Income: <span class="font-bold">{{ formatAmount(budgettotalIncomes) }} </span></div>-->
<!-- <div class="mb-4">--> <!-- <div class="text-sm">Total Expenses: <span class="font-bold">{{ formatAmount(budget.totalExpenses) }} </span></div>-->
<!-- <div class="text-sm">Total Income: <span class="font-bold">{{ formatAmount(budgettotalIncomes) }} </span></div>--> <!-- <div class="text-sm">Planned 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>--> <!-- <div class="text-sm flex items-center">-->
<!-- <div class="text-sm">Planned Expenses: <span class="font-bold">{{ formatAmount(budget.totalExpenses) }} </span></div>--> <!-- Unplanned Expenses:-->
<!-- <div class="text-sm flex items-center">--> <!-- <span class="ml-2 font-bold">{{ formatAmount(budget.totalIncomes - budget.totalExpenses) }} </span>-->
<!-- Unplanned Expenses:--> <!-- Прогресс бар -->
<!-- <span class="ml-2 font-bold">{{ formatAmount(budget.totalIncomes - budget.totalExpenses) }} </span>--> <!-- <ProgressBar :value="budget.unplannedProgress" class="ml-4 w-full"/>-->
<!-- Прогресс бар --> <!-- </div>-->
<!-- <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 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-xl font-bold mb-2">{{ budget.month }}</div>-->
<!-- <div class="text-sm text-gray-600 mb-4">--> <!-- <div class="text-sm text-gray-600 mb-4">-->
<!-- {{ budget.startDate }} - {{ budget.endDate }}--> <!-- {{ budget.startDate }} - {{ budget.endDate }}-->
<!-- </div>--> <!-- </div>-->
<!-- <div class="mb-4">--> <!-- <div class="mb-4">-->
<!-- <div class="text-sm">Total Income: <span class="font-bold">{{ budget.totalIncome }}</span></div>--> <!-- <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">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">Planned Expenses: <span class="font-bold">{{ budget.plannedExpenses }}</span></div>-->
<!-- <div class="text-sm flex items-center">--> <!-- <div class="text-sm flex items-center">-->
<!-- Unplanned Expenses:--> <!-- Unplanned Expenses:-->
<!-- <span class="ml-2 font-bold">{{ budget.remainingForUnplanned }}</span>--> <!-- <span class="ml-2 font-bold">{{ budget.remainingForUnplanned }}</span>-->
<!-- &lt;!&ndash; Прогресс бар &ndash;&gt;--> <!-- &lt;!&ndash; Прогресс бар &ndash;&gt;-->
<!-- <ProgressBar :value="budget.unplannedProgress" class="ml-4 w-full"/>--> <!-- <ProgressBar :value="budget.unplannedProgress" class="ml-4 w-full"/>-->
<!-- </div>--> <!-- </div>-->
<!-- </div>--> <!-- </div>-->
<!-- </div>--> <!-- </div>-->
</div> </div>
</div> </div>
</template> </template>
<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) => {
budgetInfos.value = await getBudgetInfos()
creationSuccessModal.value = true
setTimeout(() => { try {
creationSuccessModal.value = false await createBudget(budget, createRecurrentPayments)
} budgetInfos.value = await getBudgetInfos()
, 1000) 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});
}
// 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 (drawerOpened.value) { if (props.transaction?.parentId) {
drawerOpened.value = false; toast.add({
severity: 'warn',
summary: 'Транзакцию нельзя изменить!',
detail: 'Транзакции созданные из плана не могут быть изменены.',
life: 3000
});
} else {
if (drawerOpened.value) {
drawerOpened.value = false;
}
drawerOpened.value = !drawerOpened.value
emits('open-drawer', props.transaction)
} }
drawerOpened.value = !drawerOpened.value
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">
@@ -138,7 +157,7 @@ onMounted(async () => {
<TransactionForm v-if="drawerOpened" :visible="drawerOpened" :transaction="transaction" <TransactionForm v-if="drawerOpened" :visible="drawerOpened" :transaction="transaction"
@close-drawer="closeDrawer" @transaction-updated="transactionUpdate" @close-drawer="closeDrawer" @transaction-updated="transactionUpdate"
@delete-transaction="transactionUpdate" @delete-transaction="transactionUpdate"
@create-transaction="transactionUpdate"/> @create-transaction="transactionUpdate"/>
</div> </div>

View File

@@ -15,11 +15,44 @@
/> />
</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 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> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
@@ -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>
@@ -136,7 +172,7 @@
<div class="flex flex-col items-center "> <div class="flex flex-col items-center ">
<span class="text-sm lg:text-base">Факт. траты 📛</span> <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"> <div class="font-light bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center">
{{ formatAmount(totalInstantExpenses)}} {{ formatAmount(totalInstantExpenses) }}
</div> </div>
</div> </div>
@@ -144,7 +180,7 @@
<div class="flex flex-col items-center w-full "> <div class="flex flex-col items-center w-full ">
<span class="text-sm lg:text-base">Остаток на траты</span> <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"> <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> </div>
<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'">
@@ -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>
@@ -288,16 +331,16 @@
</div> </div>
</div> </div>
<!-- <TransactionEditDrawer v-if="drawerOpened" :visible="drawerOpened"--> <!-- <TransactionEditDrawer v-if="drawerOpened" :visible="drawerOpened"-->
<!-- :transaction-type="transactionType"--> <!-- :transaction-type="transactionType"-->
<!-- :category-type="categoryType"--> <!-- :category-type="categoryType"-->
<!-- :transactions="transactions.slice(0,3)"--> <!-- :transactions="transactions.slice(0,3)"-->
<!-- @transaction-updated="updateTransactions"--> <!-- @transaction-updated="updateTransactions"-->
<!-- @delete-transaction="updateTransactions"--> <!-- @delete-transaction="updateTransactions"-->
<!-- @create-transaction="updateTransactions"--> <!-- @create-transaction="updateTransactions"-->
<!-- @close-drawer="closeDrawer"--> <!-- @close-drawer="closeDrawer"-->
<!-- />--> <!-- />-->
<TransactionForm v-if="drawerOpened" :visible="drawerOpened" :transaction-type="transactionType" <TransactionForm v-if="drawerOpened" :visible="drawerOpened" :transaction-type="transactionType"
:category-type="categoryType" @close-drawer="closeDrawer" @transaction-updated="updateTransactions" :category-type="categoryType" @close-drawer="closeDrawer" @transaction-updated="updateTransactions"
@@ -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
@@ -391,18 +451,18 @@ const totalLoans = computed(() => {
const loansRatio = 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(() => { 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(() => { 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
@@ -430,7 +490,7 @@ const closeDrawer = async () => {
} }
const dailyRatio = computed(() => { 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 return value * 100
}) })
@@ -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
@@ -487,14 +547,14 @@ const transactions = ref<Transaction[]>([])
const selectedCategoryId = ref() const selectedCategoryId = ref()
const selectCategoryType = (categoryId) => { const selectCategoryType = (categoryId) => {
if (selectedCategoryId.value==categoryId) { if (selectedCategoryId.value == categoryId) {
selectedCategoryId.value = null selectedCategoryId.value = null
} else { } else {
selectedCategoryId.value = categoryId selectedCategoryId.value = categoryId
} }
} }
const filteredTransactions = computed(() => { 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 () => { const fetchBudgetTransactions = async () => {
transactions.value = await getBudgetTransactions(route.params.id, 'INSTANT') transactions.value = await getBudgetTransactions(route.params.id, 'INSTANT')
@@ -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,141 +115,148 @@ 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 entireCategories = ref<Category[]>([]);
const expenseCategories = ref<Category[]>([]);
const incomeCategories = ref<Category[]>([]);
const editingCategory = ref<Category | null>(null);
const isDialogVisible = ref(false);
const fetchCategories = async () => {
loading.value = true
try {
const response = await getCategories();
entireCategories.value = response.data
expenseCategories.value = response.data.filter((category: Category) => category.type.code === 'EXPENSE');
incomeCategories.value = response.data.filter((category: Category) => category.type.code === 'INCOME');
} catch (error) {
console.error('Error fetching categories:', error);
}
loading.value = false
};
const categoryTypes = ref<CategoryType[]>([]); const loading = ref(true);
const selectedCategoryType = ref<CategoryType | null>(null); const entireCategories = ref<Category[]>([]);
const fetchCategoryTypes = async () => { const expenseCategories = ref<Category[]>([]);
loading.value = true const incomeCategories = ref<Category[]>([]);
try { const editingCategory = ref<Category | null>(null);
const response = await getCategoryTypes(); const isDialogVisible = ref(false);
categoryTypes.value = response.data; const confirm = useConfirm();
selectedCategoryType.value = categoryTypes.value.find((category: CategoryType) => category.code === 'EXPENSE'); const toast = useToast();
} catch (error) {
console.error('Error fetching category types:', error);
}
loading.value = false
};
const searchTerm = ref(''); const fetchCategories = async () => {
loading.value = true
const filteredExpenseCategories = computed(() => try {
expenseCategories.value.filter(category => const response = await getCategories();
category.name.toLowerCase().includes(searchTerm.value.toLowerCase()) entireCategories.value = response.data
) expenseCategories.value = response.data.filter((category: Category) => category.type.code === 'EXPENSE');
); incomeCategories.value = response.data.filter((category: Category) => category.type.code === 'INCOME');
} catch (error) {
const filteredIncomeCategories = computed(() => console.error('Error fetching categories:', error);
incomeCategories.value.filter(category => }
category.name.toLowerCase().includes(searchTerm.value.toLowerCase()) loading.value = false
)
);
const filteredCategories = computed(() => {
if (selectedCategoryType.value?.code === 'EXPENSE') {
return filteredExpenseCategories.value;
} else {
return filteredIncomeCategories.value;
}
});
const openCreateDialog = (categoryType: CategoryType | null = null) => {
if (categoryType) {
selectedCategoryType.value = categoryType;
} else if (editingCategory.value) {
selectedCategoryType.value = editingCategory.value.type;
} else {
selectedCategoryType.value = categoryTypes.value.find(category => category.code === 'EXPENSE');
}
isDialogVisible.value = true;
};
const closeCreateDialog = () => {
isDialogVisible.value = false;
editingCategory.value = null; // Сбрасываем категорию при закрытии
};
const saveCategory = async (newCategory: Category) => {
if (newCategory.id) {
await updateCategory(newCategory.id, newCategory);
} else {
await createCategory(newCategory);
}
await fetchCategories()
closeCreateDialog();
};
const deleteCat = async (categoryId: number) => {
await deleteCategory(categoryId);
await fetchCategories();
}
const openEdit = (category: Category) => {
editingCategory.value = category;
nextTick(() => {
openCreateDialog(category.type); // Обновляем форму для редактирования
});
};
watch(editingCategory, (newCategory) => {
if (newCategory) {
selectedCategoryType.value = newCategory.type; // Обновляем тип при редактировании
}
});
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,
};
},
}; };
const categoryTypes = ref<CategoryType[]>([]);
const selectedCategoryType = ref<CategoryType | null>(null);
const fetchCategoryTypes = async () => {
loading.value = true
try {
const response = await getCategoryTypes();
categoryTypes.value = response.data;
selectedCategoryType.value = categoryTypes.value.find((category: CategoryType) => category.code === 'EXPENSE');
} catch (error) {
console.error('Error fetching category types:', error);
}
loading.value = false
};
const searchTerm = ref('');
const filteredExpenseCategories = computed(() =>
expenseCategories.value.filter(category =>
category.name.toLowerCase().includes(searchTerm.value.toLowerCase())
)
);
const filteredIncomeCategories = computed(() =>
incomeCategories.value.filter(category =>
category.name.toLowerCase().includes(searchTerm.value.toLowerCase())
)
);
const filteredCategories = computed(() => {
if (selectedCategoryType.value?.code === 'EXPENSE') {
return filteredExpenseCategories.value;
} else {
return filteredIncomeCategories.value;
}
});
const openCreateDialog = (categoryType: CategoryType | null = null) => {
if (categoryType) {
selectedCategoryType.value = categoryType;
} else if (editingCategory.value) {
selectedCategoryType.value = editingCategory.value.type;
} else {
selectedCategoryType.value = categoryTypes.value.find(category => category.code === 'EXPENSE');
}
isDialogVisible.value = true;
};
const closeCreateDialog = () => {
isDialogVisible.value = false;
editingCategory.value = null; // Сбрасываем категорию при закрытии
};
const saveCategory = async (newCategory: Category) => {
if (newCategory.id) {
await updateCategory(newCategory.id, newCategory);
} else {
await createCategory(newCategory);
}
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) => {
await deleteCategory(categoryId);
await fetchCategories();
}
const openEdit = (category: Category) => {
editingCategory.value = category;
nextTick(() => {
openCreateDialog(category.type); // Обновляем форму для редактирования
});
};
watch(editingCategory, (newCategory) => {
if (newCategory) {
selectedCategoryType.value = newCategory.type; // Обновляем тип при редактировании
}
});
onMounted(async () => {
await fetchCategories();
await fetchCategoryTypes();
loading.value = false;
});
</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,26 +14,26 @@
<!-- 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>
<script lang="ts"> <script lang="ts">
import {ref, watch, computed, PropType} from 'vue'; import {ref, watch, computed, PropType} from 'vue';
import Button from "primevue/button"; import Button from "primevue/button";
import Dialog from "primevue/dialog"; import Dialog from "primevue/dialog";
import SelectButton from "primevue/selectbutton"; import SelectButton from "primevue/selectbutton";
@@ -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
} }
}); });
@@ -71,7 +67,7 @@ const categoryTypes = ref<CategoryType[]>([]);
const transactionTypes = ref<TransactionType[]>([]); const transactionTypes = ref<TransactionType[]>([]);
const userStore = useUserStore(); const userStore = useUserStore();
const user = computed( () => userStore.user) const user = computed(() => userStore.user)
const isReady = computed(() => !loading.value && loadingUser.value) const isReady = computed(() => !loading.value && loadingUser.value)
@@ -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,167 +269,170 @@ 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);
console.log(transactions.value.slice(0,3)) // transactions.value = 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>
<template> <template>
<div class="card flex justify-center h-fit"> <div class="card flex justify-center h-fit">
<div v-if="result" class="absolute top-0 left-0 w-full h-full flex items-center justify-center z-50"> <div v-if="result" class="absolute top-0 left-0 w-full h-full flex items-center justify-center z-50">
<div <div
class=" px-10 py-5 rounded-lg border border-gray-200 flex flex-col items-center gap-4" class=" px-10 py-5 rounded-lg border border-gray-200 flex flex-col items-center gap-4"
:class="isError ? 'bg-red-100' : 'bg-green-100'" :class="isError ? 'bg-red-100' : 'bg-green-100'"
aria-label="Custom ProgressSpinner"> aria-label="Custom ProgressSpinner">
<i class="pi pi-check " :class="isError ? 'text-red-500' : 'text-green-500'" style="font-size: 2rem;"/> <i class="pi pi-check " :class="isError ? 'text-red-500' : 'text-green-500'" style="font-size: 2rem;"/>
<p class="text-green-700" :class="isError ? 'text-red-500' : 'text-green-500'">{{ resultText }}</p> <p class="text-green-700" :class="isError ? 'text-red-500' : 'text-green-500'">{{ resultText }}</p>
</div>
</div>
<div class="absolute w-full h-screen">
<!-- Полупрозрачный белый фон -->
<!-- <div class="absolute top-0 left-0 w-full h-full bg-white opacity-50 z-0"></div>-->
<!-- Спиннер поверх -->
</div> </div>
<LoadingView v-if="loading"/> </div>
<div v-else class=" grid gap-4 w-full "> <div class="absolute w-full h-screen">
<div class="relative w-full justify-center justify-items-center "> <!-- Полупрозрачный белый фон -->
<div class="flex flex-col justify-items-center gap-2"> <!-- <div class="absolute top-0 left-0 w-full h-full bg-white opacity-50 z-0"></div>-->
<div class="flex flex-row gap-2">
<Select v-if="!isEditing" v-model="editedTransaction.transactionType" :allow-empty="false" <!-- Спиннер поверх -->
:options="transactionTypes"
optionLabel="name" </div>
aria-labelledby="basic"
class="justify-center"/> <LoadingView v-if="loading"/>
<SelectButton v-model="selectedCategoryType" :options="categoryTypes" :allow-empty="false" <div v-else class=" grid gap-4 w-full ">
optionLabel="name" <div class="relative w-full justify-center justify-items-center ">
aria-labelledby="basic" <div class="flex flex-col justify-items-center gap-2">
@change="categoryTypeChanged" class="justify-center"/> <div class="flex flex-row gap-2">
</div> <Select v-if="!isEditing" v-model="editedTransaction.type" :allow-empty="false"
<button class="border border-gray-300 rounded-lg w-full z-40" :options="transactionTypes"
@click="isCategorySelectorOpened = !isCategorySelectorOpened"> optionLabel="name"
<div class="flex flex-row items-center pe-4 py-2 "> aria-labelledby="basic"
<div class="flex flex-row justify-between w-full gap-4 px-4 items-center"> class="justify-center"/>
<p class="text-3xl font-bold text-gray-700 dark:text-gray-400">{{ <SelectButton v-model="selectedCategoryType" :options="categoryTypes" :allow-empty="false"
editedTransaction.category.icon optionLabel="name"
aria-labelledby="basic"
@change="categoryTypeChanged" class="justify-center"/>
</div>
<button class="border border-gray-300 rounded-lg w-full z-40"
@click="isCategorySelectorOpened = !isCategorySelectorOpened">
<div class="flex flex-row items-center pe-4 py-2 ">
<div class="flex flex-row justify-between w-full gap-4 px-4 items-center">
<p class="text-3xl font-bold text-gray-700 dark:text-gray-400">{{
editedTransaction.category.icon
}}</p>
<div class="flex flex-col items-start justify-items-start justify-around w-full">
<p class="font-bold text-start line-clamp-1">{{ editedTransaction.category.name }}</p>
<p class="font-light line-clamp-1 items-start text-start">{{
editedTransaction.category.description
}}</p> }}</p>
<div class="flex flex-col items-start justify-items-start justify-around w-full">
<p class="font-bold text-start line-clamp-1">{{ editedTransaction.category.name }}</p>
<p class="font-light line-clamp-1 items-start text-start">{{
editedTransaction.category.description
}}</p>
</div>
</div> </div>
<div> </div>
<div>
<span :class="{'rotate-90': isCategorySelectorOpened}" <span :class="{'rotate-90': isCategorySelectorOpened}"
class="pi pi-angle-right transition-transform duration-300 text-5xl"/> class="pi pi-angle-right transition-transform duration-300 text-5xl"/>
</div>
</div>
</button>
</div>
<!-- Анимированное открытие списка категорий -->
<div v-show="isCategorySelectorOpened"
class="absolute left-0 right-0 top-full overflow-y-auto z-50 border-b-4 border-x rounded-b-lg bg-white shadow-lg transition-all duration-500"
:class="{ 'max-h-0': !isCategorySelectorOpened, 'max-h-[500px]': isCategorySelectorOpened }">
<div class="grid grid-cols-2 mt-2">
<button
v-for="category in editedTransaction.category.type.code == 'EXPENSE' ? expenseCategories : incomeCategories"
:key="category.id" class="border rounded-lg mx-2 mb-2"
@click="selectCategory(category)">
<div class="flex flex-row justify-between w-full px-2">
<p class="text-4xl font-bold text-gray-700 dark:text-gray-400">{{ category.icon }}</p>
<div class="flex flex-col items-start justify-items-start justify-around w-full">
<p class="font-bold text-start">{{ category.name }}</p>
<p class="font-light line-clamp-1 text-start">{{ category.description }}</p>
</div> </div>
</div> </div>
</button> </button>
</div> </div>
<!-- Анимированное открытие списка категорий -->
<div v-show="isCategorySelectorOpened"
class="absolute left-0 right-0 top-full overflow-y-auto z-50 border-b-4 border-x rounded-b-lg bg-white shadow-lg transition-all duration-500"
:class="{ 'max-h-0': !isCategorySelectorOpened, 'max-h-[500px]': isCategorySelectorOpened }">
<div class="grid grid-cols-2 mt-2">
<button
v-for="category in editedTransaction.category.type.code == 'EXPENSE' ? expenseCategories : incomeCategories"
:key="category.id" class="border rounded-lg mx-2 mb-2"
@click="selectCategory(category)">
<div class="flex flex-row justify-between w-full px-2">
<p class="text-4xl font-bold text-gray-700 dark:text-gray-400">{{ category.icon }}</p>
<div class="flex flex-col items-start justify-items-start justify-around w-full">
<p class="font-bold text-start">{{ category.name }}</p>
<p class="font-light line-clamp-1 text-start">{{ category.description }}</p>
</div>
</div>
</button>
</div>
</div>
</div> </div>
<div class="flex flex-row gap-4"> </div>
<div class="flex flex-row gap-4">
<FloatLabel variant="on" class=""> <FloatLabel variant="on" class="">
<InputNumber class="" <InputNumber class=""
:invalid="!editedTransaction.amount" ref="amountInput"
:minFractionDigits="0" :invalid="!editedTransaction.amount"
id="amount" :minFractionDigits="0"
v-model="editedTransaction.amount" id="amount"
mode="currency" v-model="editedTransaction.amount"
currency="RUB" mode="currency"
locale="ru-RU" currency="RUB"
@focus="keyboardOpen=true" locale="ru-RU"
@blur="keyboardOpen=false"
/>
<label for="amount" class="">Сумма</label>
</FloatLabel>
<!-- Comment Input -->
<FloatLabel variant="on" class="w-full">
<label for="comment">Комментарий</label>
<InputText class="w-full"
:invalid="!editedTransaction.comment"
id="comment"
v-model="editedTransaction.comment"
@focus="keyboardOpen=true" @focus="keyboardOpen=true"
@blur="keyboardOpen=false" @blur="keyboardOpen=false"
/>
</FloatLabel>
</div> />
<label for="amount" class="">Сумма</label>
</FloatLabel>
<!-- Date Picker --> <!-- Comment Input -->
<div class="field col-12 gap-0">
<FloatLabel variant="on">
<label for="date">Дата</label>
<DatePicker class="w-full" <FloatLabel variant="on" class="w-full">
inline <label for="comment">Комментарий</label>
:invalid="editedTransaction.transactionType.code != 'PLANNED' ? editedTransaction.date > new Date() : true" <InputText class="w-full"
id="date" :invalid="!editedTransaction.comment"
v-model="editedTransaction.date" id="comment"
dateFormat="yy-mm-dd" v-model="editedTransaction.comment"
showIcon @focus="keyboardOpen=true"
@blur="keyboardOpen=false"
/> />
<p :class="dateErrorMessage != '' ? 'visible' : 'invisible'" </FloatLabel>
class="text-red-400">{{ dateErrorMessage }}</p>
</FloatLabel>
</div>
<div>
<BudgetTransactionView v-if="!isEditing && transactions" v-for="transaction in transactions" :is-list="true"
:transaction="transaction"/>
</div>
<div class="fixed col-12 flex justify-content-end gap-4 bottom-8">
<Button label="Сохранить" icon="pi pi-check" class="p-button-success"
@click="isEditing ? updateTransaction() : createTransaction()"/>
<Button label="Отмена" icon="pi pi-times" class="p-button-secondary " @click="closeDrawer"/>
<Button v-if="isEditing" label="Удалить" icon="pi pi-times" class="p-button-success" severity="danger"
@click="deleteTransaction"/>
</div>
</div> </div>
<!-- Date Picker -->
<div class="field col-12 gap-0">
<FloatLabel variant="on">
<label for="date">Дата</label>
<DatePicker class="w-full"
inline
:invalid="editedTransaction.type.code != 'PLANNED' ? editedTransaction.date > new Date() : true"
id="date"
v-model="editedTransaction.date"
dateFormat="yy-mm-dd"
showIcon
/>
<p :class="dateErrorMessage != '' ? 'visible' : 'invisible'"
class="text-red-400">{{ dateErrorMessage }}</p>
</FloatLabel>
</div>
<div>
<BudgetTransactionView v-if="!isEditing && transactions" v-for="transaction in transactions" :is-list="true"
:transaction="transaction"/>
</div>
<div class="fixed col-12 flex justify-content-end gap-4 bottom-8">
<Button label="Сохранить" icon="pi pi-check" class="p-button-success"
@click="isEditing ? updateTransaction() : createTransaction()"/>
<Button label="Отмена" icon="pi pi-times" class="p-button-secondary " @click="closeDrawer"/>
<Button v-if="isEditing" label="Удалить" icon="pi pi-times" class="p-button-success" severity="danger"
@click="deleteTransaction"/>
</div>
</div>
</div> </div>
</template> </template>

View File

@@ -1,12 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from "vue"; import {computed, onMounted, ref} from "vue";
import BudgetTransactionView from "@/components/budgets/BudgetTransactionView.vue"; import BudgetTransactionView from "@/components/budgets/BudgetTransactionView.vue";
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 { Transaction } from "@/models/Transaction"; import {getTransactions} from "@/services/transactionService";
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 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 () => { onMounted(async () => {
await fetchTransactions(); // Первоначальная загрузка данных await fetchTransactions(); // Первоначальная загрузка данных
window.addEventListener("scroll", handleScroll); // Добавляем обработчик прокрутки await fetchUsers();
// window.addEventListener("scroll", handleScroll); // Добавляем обработчик прокрутки
}); });
</script> </script>
@@ -84,20 +119,32 @@ 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"
fill="transparent" fill="transparent"
animationDuration=".5s" /> animationDuration=".5s"/>
</div> </div>
</div> </div>
</div> </div>

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,23 +1,24 @@
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;
} }
export class TransactionCategoriesSum{ export class TransactionCategoriesSum {
category: Category; category: Category;
sum: number sum: number
} }
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 data = {
budget: budget, let budgetToCreate = JSON.parse(JSON.stringify(budget));
createRecurrent: createRecurrent budgetToCreate.dateFrom = format(budget.dateFrom, 'yyyy-MM-dd')
} budgetToCreate.dateTo = format(budget.dateTo, 'yyyy-MM-dd')
await apiClient.post('/budgets', data); let data = {
budget.dateFrom = format(budget.dateFrom, 'dd.mm.yy') budget: budgetToCreate,
budget.dateTo = format(budget.dateTo, 'dd.mm.yy') createRecurrent: createRecurrent
}
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) => { 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,22 +37,25 @@ 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
}); });
} }
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}`);
@@ -74,7 +81,7 @@ export const getTransactionTypes = async () => {
return await apiClient.get('/transactions/types'); return await apiClient.get('/transactions/types');
} }
export const getTransactionCategoriesSums = async () => { export const getTransactionCategoriesSums = async () => {
let response = await apiClient.get('/transactions/categories/_calc_sums'); let response = await apiClient.get('/transactions/categories/_calc_sums');
return response.data; return response.data;
} }

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