chet novoe

This commit is contained in:
Vladimir Voronin
2024-10-29 16:06:45 +03:00
parent c5ee833ca7
commit a71f34a6ba
22 changed files with 912 additions and 425 deletions

View File

@@ -18,8 +18,8 @@
tg.expand(); // Разворачиваем веб-приложение на весь экран
// Получаем информацию о пользователе и выводим её
const user = tg.initDataUnsafe.user;
console.log(user);
// const user = tg.initDataUnsafe.user;
}
</script>
<div id="app"></div>

View File

@@ -3,16 +3,6 @@
"short_name": "Luminic Space",
"start_url": "/",
"icons": [
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"display": "standalone",
"background_color": "#ffffff",

15
package-lock.json generated
View File

@@ -12,6 +12,7 @@
"@vue/cli-service": "^5.0.8",
"axios": "^1.7.7",
"chart.js": "^4.4.4",
"chartjs-plugin-datalabels": "^2.2.0",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"platform": "^1.3.6",
@@ -2153,6 +2154,14 @@
"pnpm": ">=8"
}
},
"node_modules/chartjs-plugin-datalabels": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/chartjs-plugin-datalabels/-/chartjs-plugin-datalabels-2.2.0.tgz",
"integrity": "sha512-14ZU30lH7n89oq+A4bWaJPnAG8a7ZTk7dKf48YAzMvJjQtjrgg5Dpk9f+LbjCF6bpx3RAGTeL13IXpKQYyRvlw==",
"peerDependencies": {
"chart.js": ">=3.0.0"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz",
@@ -11237,6 +11246,12 @@
"@kurkle/color": "^0.3.0"
}
},
"chartjs-plugin-datalabels": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/chartjs-plugin-datalabels/-/chartjs-plugin-datalabels-2.2.0.tgz",
"integrity": "sha512-14ZU30lH7n89oq+A4bWaJPnAG8a7ZTk7dKf48YAzMvJjQtjrgg5Dpk9f+LbjCF6bpx3RAGTeL13IXpKQYyRvlw==",
"requires": {}
},
"chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz",

View File

@@ -12,6 +12,7 @@
"@vue/cli-service": "^5.0.8",
"axios": "^1.7.7",
"chart.js": "^4.4.4",
"chartjs-plugin-datalabels": "^2.2.0",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"platform": "^1.3.6",

View File

@@ -8,7 +8,7 @@
<!-- Контентная часть заполняет оставшееся пространство -->
<div class="flex-grow ">
<router-view class="w-full h-full mt-4 lg:mt-0"/>
<router-view />
</div>
<OverlayView class="w-full sticky invisible lg:visible top-0 z-10"/>
</div>

View File

@@ -8,6 +8,19 @@
width: 7rem;
}
.p-progressbar-label {
width: 100%;
justify-content: left !important;
padding-left: 1rem;
}
.p-progressbar {
height: 0.5rem !important;
}
canvas {
/*margin-top: 1rem;*/
/*height: 12 8px !important*/
}
/*#app {*/
/* !*max-width: 1280px;*!*/
/* !*margin: 0 auto;*!*/

View File

@@ -58,9 +58,9 @@ const items = ref([
onMounted(() => {
setTimeout(() => {
console.log(route.params['mode']);
if (route.params['mode']) {
console.log(route.params['mode']);
if (route.path == '/transactions/create') {
openDrawer('INSTANT')

View File

@@ -151,15 +151,15 @@ const onAddClick = () => {
};
onMounted(() => {
setTimeout(() => {
console.log(route.params['mode']);
if (route.params['mode']) {
console.log(route.params['mode']);
openDrawer('INSTANT')
}
}, 10)
// setTimeout(() => {
// console.log(route.params['mode']);
// if (route.params['mode']) {
// console.log(route.params['mode']);
//
// openDrawer('INSTANT')
//
// }
// }, 10)
})
</script>

View File

@@ -0,0 +1,86 @@
<script setup lang="ts">
import Button from "primevue/button";
import InputNumber from "primevue/inputnumber";
import ProgressBar from "primevue/progressbar";
import {Category} from "@/models/Category";
import {computed, ref} from "vue";
import {formatAmount} from "@/utils/utils";
const props = defineProps({
category: {
type: Object as Category,
require: true
},
budgetId: {
type: Number,
require: true
}
})
const emits = defineEmits(['category-updated'])
const isEditing = ref(false);
const startEditing = () => {
isEditing.value = true;
}
const stopEditing = () => {
isEditing.value = false;
emits('category-updated', editedCategory.value);
}
// const selectedCategorySettingType = ref(props.category.categorySetting.type)
const categoryAmount = ref(1000)
const editedCategory = ref(props.category);
const spentPlannedRatio = computed( () => {
if (editedCategory.value.currentLimit == 0){
return 0;
} else {
return editedCategory.value.currentSpent / editedCategory.value.currentLimit * 100
}
})
</script>
<template>
<div class="p-2 shadow-lg rounded-lg bg-white flex justify-between flex-col ">
<div class="flex flex-row justify-between w-full">
<div :class="isEditing ? 'w-1/5': ''" class="min-w-1 w-4/6 justify-between ">
<h4 class="text-lg line-clamp-1">{{editedCategory.category.icon }} {{ editedCategory.category.name }}</h4>
<!-- <p class="text-sm text-gray-500 line-clamp-1 min-w-1 ">{{ editedCategory.category.description }}</p>-->
</div>
<div class="flex flex-row gap-2 justify-end items-center w-3/6 ">
<!-- Сумма, которая становится редактируемой при клике -->
<button v-if="!isEditing" @click="startEditing"
class="text-lg font-bold cursor-pointer w-fit text-end line-clamp-1">
<div class="flex flex-row gap-2 items-baseline" >
<p class="font-light text-sm" :class="spentPlannedRatio == 0 ? 'hidden': ''">{{spentPlannedRatio.toFixed(0)}} %</p>
<p class="line-clamp-1 w-fit">{{ formatAmount(editedCategory.currentSpent) }} /
{{ formatAmount(editedCategory.currentLimit) }} </p>
</div>
</button>
<InputNumber v-else ref="inputRefs" type="text" v-model="editedCategory.currentLimit"
class="text-lg font-bold border-b-2 border-gray-300 outline-none focus:border-blue-500 w-32 text-right"
:min="editedCategory.categoryPlannedLimit" :max="900000" :invalid="editedCategory.currentLimit < editedCategory.categoryPlannedLimit" v-tooltip.top="'Сумма не должна быть ниже суммы запланированных!'"/>
<Button v-if="isEditing" @click="stopEditing" icon="pi pi-check" severity="success" rounded outlined
aria-label="Search"/>
</div>
</div>
<div class="flex flex-col w-full">
<ProgressBar :value="Number(spentPlannedRatio.toFixed(0))" class="w-full" :show-value="false"> </ProgressBar>
<!-- <div class="z-50">{{formatAmount(spentPlannedRatio.toFixed(0))}}%</div>-->
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -115,7 +115,7 @@ const pastBudgets = ref([
onMounted(async () => {
loading.value = true;
budgetInfos.value = await getBudgetInfos()
console.log(budgetInfos.value)
loading.value = false
})
</script>

View File

@@ -16,15 +16,20 @@ const props = defineProps(
transaction: {
type: Object as PropType<Transaction>,
required: true,
},
isList: {
type: Boolean,
required: true,
}
}
)
const emits = defineEmits(['open-drawer'])
const emits = defineEmits(['open-drawer', 'transaction-checked', 'transaction-updated'])
const setIsDoneTrue = async () => {
setTimeout(async () => {
await updateTransactionRequest(props.transaction)
emits('transaction-checked')
}, 10);
// showedTransaction.value.isDone = !showedTransaction.value.isDone;
@@ -40,6 +45,11 @@ const toggleDrawer = () => {
emits('open-drawer', props.transaction)
}
const transactionUpdate = () => {
console.log('my tut 1')
emits('transaction-updated')
}
const isPlanned = computed(() => {
return props.transaction?.transactionType.code === "PLANNED"
})
@@ -86,25 +96,25 @@ onMounted(async () => {
</script>
<template>
<div :class="transaction.category.type.code == 'INCOME' ? 'from-green-100 to-green-50' : ' from-red-100 to-red-50' &&
transaction.transactionType.code == 'INSTANT' ? ' bg-gradient-to-r shadow-lg border-2 gap-5 p-2 rounded-xl ' : 'border-b pb-2'
<div :class="
props.isList ? ' bg-gradient-to-r shadow-lg border-2 gap-5 p-2 rounded-xl me-5' : 'border-b pb-2'
"
class="flex bg-white min-w-fit max-h-fit flex-row items-center gap-4 w-full ">
<div>
<p v-if="transaction.transactionType.code=='INSTANT'"
<p v-if="transaction.transactionType.code=='INSTANT' || props.isList"
class="text-6xl font-bold text-gray-700 dark:text-gray-400">
{{ transaction.category.icon }}</p>
<Checkbox v-model="transaction.isDone" v-else-if="transaction.transactionType.code=='PLANNED'"
<Checkbox v-model="transaction.isDone" v-else-if="transaction.transactionType.code=='PLANNED' && !props.isList"
:binary="true"
@click="setIsDoneTrue"/>
</div>
<button class="flex flex-row items-center p-x-4 justify-between w-full " @click="toggleDrawer">
<div class="flex flex-col items-start justify-items-start">
<p :class="transaction.isDone && isPlanned ? 'line-through' : ''" class="font-bold">{{
<p :class="transaction.isDone && isPlanned && !props.isList ? 'line-through' : ''" class="font-bold">{{
transaction.comment
}}</p>
<p :class="transaction.isDone && isPlanned ? 'line-through' : ''" class="font-light">{{
<p :class="transaction.isDone && isPlanned && !props.isList ? 'line-through' : ''" class="font-light">{{
transaction.category.name
}} |
{{ formatDate(transaction.date) }}</p>
@@ -122,7 +132,7 @@ onMounted(async () => {
<TransactionEditDrawer v-if="drawerOpened" :visible="drawerOpened" :expenseCategories="expenseCategories"
:incomeCategories="incomeCategories" :transaction="transaction"
:category-types="categoryTypes"
@transaction-updated="transactionUpdate"
@close-drawer="closeDrawer()"
/>
</div>

View File

@@ -16,169 +16,221 @@
</div>
<div :class="!updateLoading ? '' : 'h-fit bg-white opacity-50 z-0 '" class=" flex flex-col gap-3">
<div class="flex flex-col ">
<h2 class="text-4xl font-bold">Budget for {{ budgetInfo.budget.name }} </h2>
<div class="flex flex-row gap-2 text-xl">{{ formatDate(budgetInfo.budget.dateFrom) }} -
{{ formatDate(budgetInfo.budget.dateTo) }}
</div>
<!-- {{ budget }}-->
<h2 class="text-4xl font-bold">Budget for {{ budget.name }} </h2>
<!-- <div class="flex flex-row gap-2 text-xl">{{ formatDate(budget.dateFrom) }} - -->
<!-- {{ formatDate(budget.dateTo) }}-->
<!-- </div> -->
</div>
<div class="flex flex-col gap-2">
<!-- Аналитика и плановые доходы/расходы -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4 items-start ">
<!-- Блок Аналитики (25%) -->
<div class="card p-4 shadow-lg rounded-lg col-span-2 h-fit">
<h3 class="text-xl mb-4 font-bold">Analytics</h3>
<div class="flex ">
<div class="w-128">
<Chart type="bar" :data="incomeExpenseChartData" class=""/>
<!-- Аналитика и плановые доходы/расходы -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4 items-start ">
<div class="flex flex-col gap-4">
<!-- Блок Аналитики (25%) -->
<div
class="bg-white p-4 shadow-lg rounded-lg flex flex-col gap-4 items-start ">
<h3 class="text-xl font-bold">Аналитика</h3>
<SelectButton v-model="selectedChart" :options="modes" optionLabel="label" optionIcon="icon"> <template #option="slotProps">
<i :class="slotProps.option.icon"></i>
</template></SelectButton>
<Chart v-if="selectedChart.value=='bar'" type="bar" :data="incomeExpenseChartData" :options="incomeExpenseChartOptions" class="!w-full"
style="width: 100%"/>
<Chart v-if="selectedChart.value=='pie'" type="pie" :data="pieChartData" :options="pieChartOptions" class="chart "/>
<div class="flex gap-5 items-center justify-items-center ">
<div class="w-full">
<button class="grid grid-cols-2 gap-5 items-center w-full" @click="detailedShowed = !detailedShowed">
<div class="flex flex-col items-center font-bold ">
<h4 class="text-xl font-bold text-green-500">Сумма поступлений</h4>
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center">
+{{ formatAmount(totalIncomes) }}
</div>
<!-- <p>Total Incomes</p>-->
</div>
<div class="flex flex-col items-center ">
<h4 class="text-xl font-bold text-red-500">Сумма трат</h4>
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center">
-{{ formatAmount(totalExpenses) }}
</div>
</div>
</button>
<div class="grid grid-cols-2 !gap-1 mt-4" :class="detailedShowed ? 'block' : 'hidden'">
<div class="flex flex-col items-center font-bold ">
<p class="font-bold ">Поступление в первый период</p>
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner text-center w-full ">
+{{ formatAmount(incomesByPeriod[0]) }}
</div>
</div>
<div class="flex flex-col items-center">
<p class="font-bold ">Траты в первый период</p>
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner text-center w-full ">
-{{ formatAmount(expensesByPeriod[0]) }}
</div>
</div>
<div class="flex flex-col items-center">
<p class="font-bold ">Поступления во второй период</p>
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner text-center w-full ">
+{{ formatAmount(incomesByPeriod[1]) }}
</div>
</div>
<div class="flex flex-col items-center">
<p class="font-bold ">Траты во второй период</p>
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner text-center w-full ">
-{{ formatAmount(expensesByPeriod[1]) }}
</div>
</div>
</div>
</div>
</div>
<div class="grid gap-5 items-center justify-items-center ">
<div class="w-full">
<button class="grid grid-cols-3 justify-between gap-5 items-center w-full"
@click="detailedShowed = !detailedShowed">
<div class="flex flex-col items-center">
<h4 class="text-lg">Долги</h4>
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center">
{{ loansRatio.toFixed(0) }} %
</div>
<!-- <p>Total Incomes</p>-->
</div>
<div class="flex flex-col items-center ">
<h4 class="text-lg ">Сбережения</h4>
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center">
{{ savingRatio.toFixed(0) }} %
</div>
</div>
<div class="flex flex-col items-center ">
<h4 class="text-lg ">Ежедневные</h4>
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center">
{{ dailyRatio.toFixed(0) }} %
</div>
</div>
</button>
<div class="grid grid-cols-2 !gap-1 mt-4" :class="detailedShowed ? 'block' : 'hidden'">
<div v-for="transaction in transactionCategoriesSums" class="flex flex-col items-center font-bold ">
<p class="font-bold ">{{ transaction.category.name }}</p>
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner text-center w-full ">
{{ formatAmount(transaction.sum) }}
</div>
</div>
</div>
</div>
</div>
<ProgressBar :value="value" class="mt-2 col-span-2" style="height: 1rem !important;"></ProgressBar>
</div>
<div class="h-fit">
<!-- <Chart type="pie" :data="incomeExpenseChartData" class="h-64"/>-->
<div class=" h-full overflow-y-auto gap-4 flex-col row-span-6 hidden lg:flex">
<div class="flex flex-row ">
<h3 class="text-2xl font-bold mb-4 ">Transactions List</h3>
</div>
<div class=" flex gap-2">
<button v-for="categorySum in transactionCategoriesSums" :key="categorySum.category.id"
class="rounded-full border p-1 bg-white border-gray-300 mb-2 px-2">
<strong>{{ categorySum.category.name }}</strong>:
{{ categorySum.sum }}
</button>
</div>
<div class="grid grid-cols-1 gap-4 max-h-tlist overflow-y-auto pe-2">
<BudgetTransactionView v-for="transaction in transactions" :key="transaction.id"
:transaction="transaction"
:is-list="true" class=""
/>
</div>
</div>
</div>
<div class="grid grid-cols-1 gap-4 row-span-3">
<div class="card p-4 shadow-lg rounded-lg col-span-1 h-fit">
<!-- Планируемые доходы -->
<div>
<div class="flex flex-row gap-4 items-center">
<h3 class="text-xl font-bold text-green-500 mb-4 ">Planned Incomes</h3>
<Button icon="pi pi-plus" rounded outlined size="small"/>
</div>
<div class="grid grid-cols-2 mb-2">
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center">
{{ formatAmount(totalIncomes) }}
</div>
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center">
{{ formatAmount(totalIncomeLeftToGet) }}
</div>
</div>
<ul class="space-y-2">
<!-- {{ plannedIncomes }}-->
<BudgetTransactionView v-for="transaction in plannedIncomes" :transaction="transaction"
:is-list="false" @transaction-checked="fetchBudgetTransactions"
@transaction-updated="updateTransactions"/>
</ul>
</div>
</div>
<div class="card p-4 shadow-lg rounded-lg col-span-1 h-fit">
<!-- Планируемые расходы -->
<div class>
<h3 class="text-xl font-bold text-red-500 mb-4">Planned Expenses</h3>
<div class="grid grid-cols-2 mb-2">
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center">
{{ formatAmount(totalPlannedExpenses) }}
</div>
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center">
{{ formatAmount(totalExpenseLeftToSpend) }}
</div>
</div>
<ul class="space-y-2">
<BudgetTransactionView v-for="transaction in plannedExpenses" :transaction="transaction"
:is-list="false" @transaction-checked="fetchBudgetTransactions"
@transaction-updated="updateTransactions"/>
</ul>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5 items-center justify-items-center mt-4">
<div class="grid grid-cols-2 gap-5 items-center w-full">
<div class="flex flex-col items-center font-bold ">
<h4 class="text-xl font-bold text-green-500">Total Incomes:</h4>
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center">
+{{ formatAmount(budgetInfo.totalIncomes) }}
</div>
<!-- <p>Total Incomes</p>-->
</div>
<div class="flex flex-col items-center ">
<h4 class="text-xl font-bold text-red-500">Total Expenses:</h4>
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center">
-{{ formatAmount(budgetInfo.totalExpenses) }}
</div>
</div>
<div class="flex flex-col gap-2 items-center font-bold">
<p class="font-bold ">Income at 10th:</p>
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner text-center w-full px-2">
+{{ formatAmount(budgetInfo.chartData[0][0]) }}
</div>
</div>
<div class="flex flex-col gap-2 items-center">
<p class="font-bold ">Income at 25th:</p>
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner text-center w-full px-2">
+{{ formatAmount(budgetInfo.chartData[0][1]) }}
</div>
</div>
<div class="flex flex-col gap-2 items-center">
<p class="font-bold ">Expenses at 10th:</p>
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner text-center w-full px-2">
-{{ formatAmount(budgetInfo.chartData[1][0]) }}
</div>
</div>
<div class="flex flex-col gap-2 items-center">
<p class="font-bold ">Expenses at 25th:</p>
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner text-center w-full px-2">
-{{ formatAmount(budgetInfo.chartData[1][1]) }}
</div>
</div>
<div class="flex flex-col gap-2 items-center ">
<p class="font-bold">Left for unplanned:</p>
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center">
{{ formatAmount(leftForUnplanned) }}
</div>
</div>
<div class="flex flex-col gap-2 items-center ">
<p class="font-bold">Current spending:</p>
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center">
{{ formatAmount(leftForUnplanned) }}
</div>
</div>
<div class="row-span-1 h-fit">
<h3 class="text-2xl font-bold mb-4">Категории</h3>
<div class="grid grid-cols-1 gap-4">
<UnplannedCategoryView v-for="category in categories" :key="category.id"
:category="category" :budget-id="budget.id"
@category-updated="updateBudgetCategory"
class="p-4 shadow-lg rounded-lg bg-white flex justify-between items-center"/>
</div>
</div>
<ProgressBar :value="value" class="mt-2"></ProgressBar>
<div class="flex flex-col w-full items-end">
<div class="flex flex-row items-end ">
{{ formatAmount(leftForUnplanned) }}
<div class=" h-full overflow-y-auto gap-4 flex-col row-span-6 lg:hidden ">
<div class="flex flex-row ">
<h3 class="text-2xl font-bold mb-4 ">Transactions List</h3>
</div>
<div class=" flex gap-2">
<button v-for="categorySum in transactionCategoriesSums" :key="categorySum.category.id"
class="rounded-full border p-1 bg-white border-gray-300 mb-2 px-2">
<strong>{{ categorySum.category.name }}</strong>:
{{ categorySum.sum }}
</button>
</div>
<div class="grid grid-cols-1 gap-4 max-h-tlist overflow-y-auto pe-2">
<BudgetTransactionView v-for="transaction in transactions" :key="transaction.id"
:transaction="transaction"
:is-list="true" class=""
/>
</div>
</div>
</div>
<div class="grid grid-cols-1 gap-4 row-span-3">
<div class="card p-4 shadow-lg rounded-lg col-span-1 h-fit">
<!-- Планируемые доходы -->
<div>
<div class="flex flex-row gap-4 items-center">
<h3 class="text-xl font-bold text-green-500 mb-4 ">Planned Incomes</h3>
<Button icon="pi pi-plus" rounded outlined size="small"/>
</div>
<div class="grid grid-cols-2 mb-2">
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center">
{{ formatAmount(budgetInfo.totalIncomes) }}
</div>
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center">
{{ formatAmount(totalIncomeLeftToGet) }}
</div>
</div>
<ul class="space-y-2">
<BudgetTransactionView v-for="transaction in budgetInfo.plannedIncomes" :transaction="transaction"/>
</ul>
</div>
</div>
<div class="card p-4 shadow-lg rounded-lg col-span-1 h-fit">
<!-- Планируемые расходы -->
<div class>
<h3 class="text-xl font-bold text-red-500 mb-4">Planned Expenses</h3>
<div class="grid grid-cols-2 mb-2">
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center">
{{ formatAmount(budgetInfo.totalExpenses) }}
</div>
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center">
{{ formatAmount(totalExpenseLeftToSpend) }}
</div>
</div>
<ul class="space-y-2">
<BudgetTransactionView v-for="transaction in budgetInfo.plannedExpenses" :transaction="transaction"/>
</ul>
</div>
</div>
</div>
<div class="row-span-1 col-span-2 h-fit">
<h3 class="text-2xl font-bold mb-4">Unplanned Categories</h3>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<UnplannedCategoryView v-for="category in budgetInfo.unplannedCategories" :key="category.id"
:category="category" :budget-id="budgetInfo.budget.id"
@category-updated="updateBudgetCategory"
class="p-4 shadow-lg rounded-lg bg-white flex justify-between items-center"/>
</div>
</div>
<div class=" h-full overflow-y-auto gap-4 flex-col row-span-2 col-span-2">
<div class="flex flex-row ">
<h3 class="text-2xl font-bold mb-4 ">Transactions List</h3>
</div>
<div class=" flex gap-2">
<button v-for="categorySum in budgetInfo.transactionCategoriesSums" :key="categorySum.category.id"
class="rounded-full border p-1 bg-white border-gray-300 mb-2 px-2">
<strong>{{ categorySum.category.name }}</strong>:
{{ categorySum.sum }}
</button>
</div>
<div class="grid grid-cols-1 gap-4 max-h-tlist overflow-y-auto">
<BudgetTransactionView v-for="transaction in budgetInfo.transactions" :key="transaction.id"
:transaction="transaction"
@open-drawer="openDrawer"/>
</div>
</div>
</div>
</div>
@@ -188,40 +240,61 @@
<script setup lang="ts">
import {computed, onMounted, ref} from 'vue';
import { computed, onMounted, ref} from 'vue';
import Chart from 'primevue/chart';
import BudgetTransactionView from "@/components/budgets/BudgetTransactionView.vue";
import {CategoryType} from "@/models/Category";
import {getBudgetInfo, updateBudgetCategoryRequest} from "@/services/budgetsService";
import {BudgetInfo} from "@/models/Budget";
import {
getBudgetCategories, getBudgetCategoriesSums,
getBudgetInfo,
getBudgetTransactions,
updateBudgetCategoryRequest
} from "@/services/budgetsService";
import {Budget, BudgetCategory, BudgetInfo} from "@/models/Budget";
import {useRoute} from "vue-router";
import {formatAmount, formatDate} from "@/utils/utils";
import {formatAmount} from "@/utils/utils";
import ProgressBar from "primevue/progressbar";
import ProgressSpinner from "primevue/progressspinner";
import UnplannedCategoryView from "@/components/budgets/UnplannedCategoryView.vue";
import {TransactionType} from "@/models/Transaction";
import UnplannedCategoryView from "@/components/budgets/BudgetCategoryView.vue";
import {Transaction} from "@/models/Transaction";
import Toast from "primevue/toast";
import Button from "primevue/button";
import LoadingView from "@/components/LoadingView.vue";
import ChartDataLabels from 'chartjs-plugin-datalabels';
import {Chart as ChartJS} from 'chart.js/auto';
import SelectButton from "primevue/selectbutton";
// Зарегистрируем плагин
ChartJS.register(ChartDataLabels);
const loading = ref(true);
const updateLoading = ref(false);
const route = useRoute()
const detailedShowed = ref(false);
const selectedChart = ref({ "label": "bar", "icon": "pi pi-chart-bar", "value": "bar" });
const modes = [
{label: 'bar', icon: 'pi pi-chart-bar', value: 'bar'},
{label: 'pie', icon: 'pi pi-chart-pie', value: 'pie'}
];
const budgetInfo = ref<BudgetInfo>();
const value = ref(50)
const leftForUnplanned = ref(0)
const drawerOpened = ref(false);
const transactionType = ref<TransactionType>()
const categoryType = ref<CategoryType>()
const budget = ref<Budget>()
const plannedIncomes = ref<Transaction[]>([])
const totalIncomes = computed(() => {
let totalIncome = 0;
plannedIncomes.value.forEach((i) => {
totalIncome += i.amount
})
return totalIncome
})
const totalIncomeLeftToGet = computed(() => {
let totalIncomeLeftToGet = 0;
budgetInfo.value?.plannedIncomes.forEach(i => {
plannedIncomes.value.forEach(i => {
if (!i.isDone) {
totalIncomeLeftToGet += i.amount
}
@@ -229,93 +302,353 @@ const totalIncomeLeftToGet = computed(() => {
return totalIncomeLeftToGet
})
const totalLoans = computed(() => {
let value = 0
categories.value.filter((cat) => cat.category.id == 29).forEach(cat => {
value += cat.currentLimit
})
return value
})
const loansRatio = computed(() => {
return totalLoans.value / totalExpenses.value * 100
})
const savingRatio = computed(() => {
return totalSaving.value / totalExpenses.value * 100
})
const totalSaving = computed(() => {
let value = 0
categories.value.filter((cat) => cat.category.id == 35).forEach(cat => {
value += cat.currentLimit
})
return value
})
//
const dailyRatio = computed(() => {
const value = (totalExpenses.value - totalLoans.value - totalSaving.value) / totalExpenses.value
return value * 100
})
const fetchPlannedIncomes = async () => {
plannedIncomes.value = await getBudgetTransactions(route.params.id, 'PLANNED', 'INCOME')
}
const plannedExpenses = ref<Transaction[]>([])
const totalExpenses = computed(() => {
let totalExpense = 0;
categories.value.forEach((cat) => {
let catValue = cat.currentLimit - cat.categoryPlannedLimit
plannedExpenses.value.filter(t => t.category.id == cat.category.id).forEach((i) => {
catValue += i.amount
})
totalExpense += catValue
})
return totalExpense
})
const totalPlannedExpenses = computed(() => {
let expenses = 0
plannedExpenses.value.forEach((t) => {
expenses += t.amount
})
return expenses
})
const totalExpenseLeftToSpend = computed(() => {
let totalExpenseLeftToSpend = 0;
budgetInfo.value?.plannedExpenses.forEach(i => {
plannedExpenses.value.forEach(i => {
if (!i.isDone) {
totalExpenseLeftToSpend += i.amount
}
})
return totalExpenseLeftToSpend
})
const fetchBudgetInfo = async () => {
updateLoading.value = true
console.log('Trying to get budget Info')
budgetInfo.value = await getBudgetInfo(route.params.id);
const fetchPlannedExpenses = async () => {
plannedExpenses.value = await getBudgetTransactions(route.params.id, 'PLANNED', 'EXPENSE')
updateLoading.value = false
}
const transactions = ref<Transaction[]>([])
const fetchBudgetTransactions = async () => {
console.log(budgetInfo.value)
console.log(budgetInfo.value?.chartData[0])
incomeExpenseChartData.value = {
labels: ['10.10', '25.10'],
datasets: [
{
label: 'Income',
backgroundColor: ['#2E8B57'],
data: budgetInfo.value?.chartData[0],
},
{
label: 'Expense',
backgroundColor: ['#B22222'],
data: budgetInfo.value?.chartData[1],
},
],
}
leftForUnplanned.value =
budgetInfo.value.chartData[0][0] +
budgetInfo.value.chartData[0][1] -
budgetInfo.value.chartData[1][0] -
budgetInfo.value.chartData[1][1]
transactions.value = await getBudgetTransactions(route.params.id)
updateLoading.value = false
}
const updateTransactions = async () => {
await Promise.all([fetchPlannedIncomes(), fetchPlannedExpenses()])
}
const categories = ref<BudgetCategory[]>([])
const fetchBudgetCategories = async () => {
categories.value = await getBudgetCategories(route.params.id)
updateLoading.value = false
}
const transactionCategoriesSums = ref()
const fetchBudgetTransactionCategoriesSums = async () => {
transactionCategoriesSums.value = await getBudgetCategoriesSums(route.params.id)
updateLoading.value = false
}
const budgetInfo = ref<BudgetInfo>();
const fetchBudgetInfo = async () => {
budget.value = await getBudgetInfo(route.params.id);
updateLoading.value = false
}
const updateBudgetCategory = async (category) => {
console.log(category)
loading.value = true
await updateBudgetCategoryRequest(budgetInfo.value.budget.id, category)
budgetInfo.value = await getBudgetInfo(route.params.id)
console.log(budgetInfo.value)
console.log(budgetInfo.value?.chartData[0])
incomeExpenseChartData.value = {
labels: ['10.10', '25.10'],
datasets: [
{
label: 'Income',
backgroundColor: ['#2E8B57'],
data: budgetInfo.value?.chartData[0],
},
{
label: 'Expense',
backgroundColor: ['#B22222'],
data: budgetInfo.value?.chartData[1],
},
],
}
leftForUnplanned.value =
budgetInfo.value.chartData[0][0] +
budgetInfo.value.chartData[0][1] -
budgetInfo.value.chartData[1][0] -
budgetInfo.value.chartData[1][1]
loading.value = false
// loading.value = true
await updateBudgetCategoryRequest(budget.value.id, category)
// categories.value = await getBudgetCategories(route.params.id)
// incomeExpenseChartData.value = {
// labels: ['10.10', '25.10'],
// datasets: [
// {
// label: 'Income',
// backgroundColor: ['#2E8B57'],
// data: budgetInfo.value?.chartData[0],
// },
// {
// label: 'Expense',
// backgroundColor: ['#B22222'],
// data: budgetInfo.value?.chartData[1],
// },
// ],
// }
// leftForUnplanned.value =
// budgetInfo.value.chartData[0][0] +
// budgetInfo.value.chartData[0][1] -
// budgetInfo.value.chartData[1][0] -
// budgetInfo.value.chartData[1][1]
// // loading.value = false
}
// Пример данных
const incomeExpenseChartData = ref();
const twentyFour = computed(() => {
let twentyFour = new Date(budget.value?.dateFrom)
twentyFour.setDate(24)
return twentyFour
})
const incomesByPeriod = computed(() => {
let incomesUntil25 = 0
let incomesFrom25 = 0
plannedIncomes.value.forEach((i) => {
if (i.date <= budget.value?.dateFrom && i.date <= twentyFour.value) {
incomesUntil25 += i.amount
} else {
incomesFrom25 += i.amount
}
})
return [incomesUntil25, incomesFrom25]
})
const expensesByPeriod = computed(() => {
let expensesUntil25 = 0
let expensesFrom25 = 0
plannedExpenses.value.forEach((i) => {
if (i.date >= budget.value?.dateFrom && i.date <= twentyFour.value) {
expensesUntil25 += i.amount
} else {
expensesFrom25 += i.amount
}
})
categories.value.forEach((i) => {
expensesUntil25 += (i.currentLimit - i.categoryPlannedLimit) / 2
expensesFrom25 += (i.currentLimit - i.categoryPlannedLimit) / 2
})
return [expensesUntil25, expensesFrom25]
})
const pieChartData = computed(() => {
let labels = []
let values = []
categories.value.forEach((i) => {
labels.push(i.category.name)
values.push(i.currentLimit / totalExpenses.value)
})
return {
labels: labels,
datasets: [
{
clip: {left: 100, top: false, right: 50, bottom: 0},
radius: '100%',
data: values,
backgroundColor: [
'rgba(6, 182, 212, 0.2)', 'rgba(249, 115, 22, 0.2)', 'rgba(50, 205, 50, 0.2)',
'rgba(255, 99, 132, 0.2)', 'rgba(54, 162, 235, 0.2)', 'rgba(255, 206, 86, 0.2)',
'rgba(75, 192, 192, 0.2)', 'rgba(153, 102, 255, 0.2)', 'rgba(255, 159, 64, 0.2)',
'rgba(123, 239, 178, 0.2)', 'rgba(255, 105, 180, 0.2)', 'rgba(147, 112, 219, 0.2)',
'rgba(60, 179, 113, 0.2)', 'rgba(100, 149, 237, 0.2)', 'rgba(220, 20, 60, 0.2)',
'rgba(255, 140, 0, 0.2)', 'rgba(72, 61, 139, 0.2)', 'rgba(210, 105, 30, 0.2)',
'rgba(106, 90, 205, 0.2)', 'rgba(199, 21, 133, 0.2)', 'rgba(32, 178, 170, 0.2)',
'rgba(65, 105, 225, 0.2)', 'rgba(218, 165, 32, 0.2)', 'rgba(255, 127, 80, 0.2)',
'rgba(46, 139, 87, 0.2)', 'rgba(139, 69, 19, 0.2)', 'rgba(75, 0, 130, 0.2)',
'rgba(255, 69, 0, 0.2)', 'rgba(244, 164, 96, 0.2)'
],
hoverBackgroundColor: [
'rgba(6, 182, 212, 1)', 'rgba(249, 115, 22, 1)', 'rgba(50, 205, 50, 1)',
'rgba(255, 99, 132, 1)', 'rgba(54, 162, 235, 1)', 'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)', 'rgba(153, 102, 255, 1)', 'rgba(255, 159, 64, 1)',
'rgba(123, 239, 178, 1)', 'rgba(255, 105, 180, 1)', 'rgba(147, 112, 219, 1)',
'rgba(60, 179, 113, 1)', 'rgba(100, 149, 237, 1)', 'rgba(220, 20, 60, 1)',
'rgba(255, 140, 0, 1)', 'rgba(72, 61, 139, 1)', 'rgba(210, 105, 30, 1)',
'rgba(106, 90, 205, 1)', 'rgba(199, 21, 133, 1)', 'rgba(32, 178, 170, 1)',
'rgba(65, 105, 225, 1)', 'rgba(218, 165, 32, 1)', 'rgba(255, 127, 80, 1)',
'rgba(46, 139, 87, 1)', 'rgba(139, 69, 19, 1)', 'rgba(75, 0, 130, 1)',
'rgba(255, 69, 0, 1)', 'rgba(244, 164, 96, 1)'
]
}
]
};
})
const pieChartOptions = ref({
layout: {
padding: {
left: 50, // Добавляем отступ слева
right: 50 // Добавляем отступ справа
}
},
plugins: {
legend: {
display: false // Отключаем легенду
},
datalabels: {
anchor: 'end', // Позиция метки относительно данных
align: 'top', // Выравнивание метки
formatter: (value, context) => {
const label = context.chart.data.labels[context.dataIndex];
const percentage = (value * 100).toFixed(0);
return percentage >= 2 ? `${label} ${percentage}%` : '';
},
color: 'black', // Цвет текста метки
font: {
size: 12 // Устанавливаем меньший размер шрифта
}
},
tooltip: {
enabled: true, // Включить tooltips
callbacks: {
title: function (tooltipItems) {
return tooltipItems[0].dataset.label;
},
label: function (tooltipItems) {
return Number(tooltipItems.formattedValue * 100).toFixed(0) + '%';
}
}
}
}
});
const incomeExpenseChartData = computed(() => {
return {
labels: ['до 25 ', 'с 25'],
datasets: [
{
label: 'Пополнения',
data: incomesByPeriod.value,
backgroundColor: ['rgba(6, 182, 212, 0.2)', 'rgba(6, 182, 212, 0.2)'],
borderColor: ['rgb(6, 182, 212)', 'rgb(6, 182, 212)'],
borderWidth: 1
},
{
label: 'Расходы',
data: expensesByPeriod.value,
backgroundColor: ['rgba(249, 115, 22, 0.2)', 'rgba(249, 115, 22, 0.2)'],
borderColor: ['rgb(249, 115, 22)', 'rgb(249, 115, 22)'],
borderWidth: 1
}
]
}
})
const incomeExpenseChartOptions = ref({
plugins: {
legend: {
display: false // Отключаем легенду
},
datalabels: {
anchor: 'end', // Позиция метки относительно данных
align: 'top', // Выравнивание метки
formatter: (value) => {
// const label = context.chart.data.labels[context.dataIndex];
return formatAmount(value) + '₽';
// return percentage >= 2 ? `${percentage} ` : '';
},
color: 'black', // Цвет текста метки
font: {
size: 12 // Устанавливаем меньший размер шрифта
}
},
tooltip: {
enabled: true, // Включить tooltips
callbacks: {
title: function (tooltipItems) {
return tooltipItems[0].dataset.label;
},
label: function (tooltipItems) {
console.log(tooltipItems);
return formatAmount(tooltipItems.raw) + '₽';
}
}
}
}
});
onMounted(async () => {
loading.value = true;
try {
await Promise.all([
fetchBudgetInfo()
fetchBudgetInfo(),
// budget.value = await getBudgetInfo(route.params.id),
fetchPlannedIncomes(),
fetchPlannedExpenses(),
fetchBudgetCategories(),
fetchBudgetTransactions(),
fetchBudgetTransactionCategoriesSums()
]);
} catch (error) {
console.error('Error during fetching data:', error);
} finally {
loading.value = false;
loading.value = false
}
setTimeout(() => {
}, 100)
});
</script>
@@ -332,13 +665,16 @@ onMounted(async () => {
}
.max-h-tlist {
max-height: 45dvh; /* Ограничение высоты списка */
max-height: 1170px; /* Ограничение высоты списка */
}
.box-shadow-inner {
box-shadow: inset 0 2px 4px 0 rgb(0 0 0 / 0.3);
}
.chart {
width: 70%;
}
</style>

View File

@@ -40,10 +40,10 @@ const props = defineProps({
}
});
const emit = defineEmits(['create-transaction', 'update-transaction', 'delete-transaction', 'close-drawer']);
const emit = defineEmits(['create-transaction', 'update-transaction', 'delete-transaction', 'close-drawer', 'transaction-updated']);
const toast = useToast();
const categoryTypeChanged = () => {
console.log(selectedCategoryType.value)
editedTransaction.value.category = selectedCategoryType.value.code == "EXPENSE" ? expenseCategories.value[0] : incomeCategories.value[0];
}
@@ -82,11 +82,30 @@ const fetchCategoriesAndTypes = async () => {
categoryTypes.value = categoryTypesResponse.data;
transactionTypes.value = transactionTypesResponse.data;
console.log(entireCategories.value)
} catch (error) {
console.error('Error fetching categories and types:', error);
}
};
const checkForm = () => {
const errorMessages = {
transactionType: 'Тип транзакции должен быть выбран',
category: 'Категория должна быть выбрана',
date: 'Дата должна быть выбрана',
comment: 'Комментарий должен быть введен',
amount: 'Сумма не может быть пустой или 0'
};
if (!editedTransaction.value.transactionType) return showError(errorMessages.transactionType);
if (!editedTransaction.value.category) return showError(errorMessages.category);
if (!editedTransaction.value.date) return showError(errorMessages.date);
if (!editedTransaction.value.comment) return showError(errorMessages.comment);
if (!editedTransaction.value.amount || editedTransaction.value.amount === 0) return showError(errorMessages.amount);
return true;
};
// Инициализация данных
const prepareData = () => {
if (!props.transaction) {
@@ -102,39 +121,84 @@ const prepareData = () => {
}
};
const result = ref(false)
const isError = ref(false)
const resultText = ref('')
const computeResult = (resultState, error) => {
if (!resultState && error) {
result.value = true;
isError.value = true
resultText.value = `Ошибка: ${error.message}`
} else {
result.value = true;
isError.value = false
resultText.value = 'Успех!'
}
setTimeout(() => {
result.value = false
resultText.value = ''
}, 1000)
}
const showError = (message) => {
result.value = true;
isError.value = true;
resultText.value = message;
return false;
};
// Создание транзакции
const createTransaction = async () => {
try {
loading.value = true;
if (editedTransaction.value.transactionType.code === 'INSTANT') {
editedTransaction.value.isDone = true;
if (checkForm()) {
try {
loading.value = true;
if (editedTransaction.value.transactionType.code === 'INSTANT') {
editedTransaction.value.isDone = true;
}
await createTransactionRequest(editedTransaction.value);
toast.add({severity: 'success', summary: 'Transaction created!', detail: 'Транзакция создана!', life: 3000});
emit('create-transaction', editedTransaction.value);
computeResult(true)
resetForm();
} catch (error) {
computeResult(false, error)
console.error('Error creating transaction:', error);
} finally {
loading.value = false;
}
await createTransactionRequest(editedTransaction.value);
toast.add({severity: 'success', summary: 'Transaction created!', detail: 'Транзакция создана!', life: 3000});
emit('create-transaction', editedTransaction.value);
resetForm();
} catch (error) {
console.error('Error creating transaction:', error);
} finally {
loading.value = false;
console.log(editedTransaction.value)
}
setTimeout(() => {
result.value = false
resultText.value = ''
}, 1000)
};
// Обновление транзакции
const updateTransaction = async () => {
try {
loading.value = true;
const response = await updateTransactionRequest(editedTransaction.value);
editedTransaction.value = response.data;
toast.add({severity: 'success', summary: 'Transaction updated!', detail: 'Транзакция обновлена!', life: 3000});
emit('update-transaction', editedTransaction.value);
} catch (error) {
console.error('Error updating transaction:', error);
} finally {
loading.value = false;
if (checkForm()) {
try {
loading.value = true;
const response = await updateTransactionRequest(editedTransaction.value);
response.data;
// toast.add({severity: 'success', summary: 'Transaction updated!', detail: 'Транзакция обновлена!', life: 3000});
emit('update-transaction', editedTransaction.value);
emit('transaction-updated');
computeResult(true)
} catch (error) {
computeResult(false, error)
console.error('Error updating transaction:', error);
} finally {
loading.value = false;
}
}
setTimeout(() => {
result.value = false
resultText.value = ''
}, 1000)
};
// Удаление транзакции
@@ -145,7 +209,11 @@ const deleteTransaction = async () => {
toast.add({severity: 'success', summary: 'Transaction deleted!', detail: 'Транзакция удалена!', life: 3000});
emit('delete-transaction', editedTransaction.value);
closeDrawer()
computeResult(true)
} catch (error) {
computeResult(false, error)
toast.add({severity: 'warn', summary: 'Error!', detail: 'Транзакция обновлена!', life: 3000});
console.error('Error deleting transaction:', error);
} finally {
loading.value = false;
@@ -162,15 +230,15 @@ const resetForm = () => {
};
const dateErrorMessage = computed(() => {
console.log('tut')
if (editedTransaction.value.transactionType.code != 'PLANNED' && editedTransaction.value.date > new Date()) {
console.log('tut2')
return 'При мгновенных тратах дата должна быть меньше текущей!'
} else if (editedTransaction.value.transactionType.code == 'PLANNED' && editedTransaction.value.date < new Date()) {
console.log('tu3')
return 'При плановых тратах дата должна быть больше текущей!'
} else {
console.log('tu4')
return ''
}
})
@@ -183,12 +251,16 @@ const userAgent = ref(null);
// Мониторинг при монтировании
onMounted(async () => {
loading.value = true;
await fetchCategoriesAndTypes();
prepareData();
loading.value = false;
const deviceInfo = platform;
isMobile.value = deviceInfo.os.family === 'iOS' || deviceInfo.os.family === 'Android';
console.log(deviceInfo);
console.log()
})
</script>
@@ -196,14 +268,32 @@ onMounted(async () => {
<div class="card flex justify-center h-dvh">
<Drawer :visible="visible" :header="isEditing ? 'Edit Transaction' : 'Create Transaction'" :showCloseIcon="false"
position="right" @hide="closeDrawer"
class="!w-128 ">
<div v-if="result" class="absolute top-0 left-0 w-full h-full flex items-center justify-center z-50">
<div
class=" px-10 py-5 rounded-lg border border-gray-200 flex flex-col items-center gap-4"
:class="isError ? 'bg-red-100' : 'bg-green-100'"
aria-label="Custom ProgressSpinner">
<i class="pi pi-check " :class="isError ? 'text-red-500' : 'text-green-500'" style="font-size: 2rem;"/>
<p class="text-green-700" :class="isError ? 'text-red-500' : 'text-green-500'">{{ resultText }}</p>
</div>
</div>
<div class="absolute w-full h-screen">
<!-- Полупрозрачный белый фон -->
<!-- <div class="absolute top-0 left-0 w-full h-full bg-white opacity-50 z-0"></div>-->
<!-- Спиннер поверх -->
</div>
<LoadingView v-if="loading"/>
<div v-else class=" grid gap-4 w-full ">
<div class="relative w-full justify-center justify-items-center ">
{{userAgent}}
<div class="flex flex-col justify-items-center gap-2">
<div class="flex flex-row gap-2">
<!-- {{editedTransaction.value.transactionType}}-->
@@ -217,7 +307,7 @@ onMounted(async () => {
aria-labelledby="basic"
@change="categoryTypeChanged" class="justify-center"/>
</div>
<button class="border border-gray-300 rounded-lg w-full z-50"
<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">
@@ -241,7 +331,7 @@ onMounted(async () => {
<!-- Анимированное открытие списка категорий -->
<div v-show="isCategorySelectorOpened"
class="absolute left-0 right-0 top-full overflow-hidden z-50 border-b-4 border-x rounded-b-lg bg-white shadow-lg transition-all duration-500"
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
@@ -319,8 +409,7 @@ onMounted(async () => {
<!-- Buttons -->
{{ keyboardOpen }}
<div class="fixed col-12 flex justify-content-end gap-4"
:style="keyboardOpen && isMobile ? 'bottom : 350px;' :' bottom: 5rem;'">
<div class="fixed col-12 flex justify-content-end gap-4 bottom-8">
<Button label="Save" icon="pi pi-check" class="p-button-success"
@click="isEditing ? updateTransaction() : createTransaction()"/>

View File

@@ -1,93 +0,0 @@
<script setup lang="ts">
import Button from "primevue/button";
import Select from "primevue/select";
import InputNumber from "primevue/inputnumber";
import {Category} from "@/models/Category";
import {ref} from "vue";
import {formatAmount} from "@/utils/utils";
const props = defineProps({
category: {
type: Object as Category,
require: true
},
budgetId: {
type: Number,
require: true
}
})
const emits = defineEmits(['category-updated'])
const isEditing = ref(false);
const startEditing = (a) => {
isEditing.value = true;
console.log(a)
}
const stopEditing = () => {
isEditing.value = false;
emits('category-updated', editedCategory.value);
}
const selectedCategorySettingType = ref(props.category.categorySetting.type)
const categorySettingTypes = ref([
{
code: 'CONST',
name: 'По значению'
},
{
code: 'AVG',
name: 'По среднему'
},
{
code: 'PERCENT',
name: 'По проценту'
},
{
code: 'LIMIT',
name: 'По лимиту'
}
])
const categoryAmount = ref(1000)
const editedCategory = ref(props.category);
</script>
<template>
<div class="p-2 shadow-lg rounded-lg bg-white flex justify-between ">
<div :class="isEditing ? 'w-1/5': ''" class="min-w-1 w-4/6">
<h4 class="text-lg line-clamp-1">{{ editedCategory.category.name }}</h4>
<p class="text-sm text-gray-500 line-clamp-1 min-w-1 ">{{ editedCategory.category.description }}</p>
</div>
<div class="flex flex-row gap-2 justify-end w-fit ">
<Select v-if="isEditing" v-model="editedCategory.categorySetting.type" :options="categorySettingTypes"
optionLabel="name"
class="line-clamp-1 w-fit"/>
<!-- Сумма, которая становится редактируемой при клике -->
<button v-if="!isEditing" @click="startEditing"
class="text-lg font-bold cursor-pointer w-full line-clamp-1">
<p class="line-clamp-1 w-fit">{{ formatAmount(editedCategory.categorySetting.value) }} </p>
</button>
<!-- @blur="stopEditing('unplannedCategories')" @keyup.enter="stopEditing('unplannedCategories')"&ndash;&gt;-->
<InputNumber v-else ref="inputRefs" type="text" v-model="editedCategory.categorySetting.settingValue"
:disabled=" editedCategory.categorySetting.type.code == 'PERCENT' ? false : editedCategory.categorySetting.type.code == 'LIMIT' ? true : editedCategory.categorySetting.type.code == 'AVG' "
:class="editedCategory.categorySetting.type.code != 'CONST' || editedCategory.categorySetting.type.code != 'PERCENT' ? 'text-gray-500' : 'text-black' "
class="text-lg font-bold border-b-2 border-gray-300 outline-none focus:border-blue-500 text-right"
:min="0" :max="editedCategory.categorySetting.type.code == 'PERCENT' ? 100 : 9000000000"/>
<Button v-if="isEditing" @click="stopEditing" icon="pi pi-check" severity="success" rounded outlined
aria-label="Search"/>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -11,7 +11,7 @@ const recurrentPayments = ref<RecurrentPayment[]>([]);
const fetchRecurrentPayments = async () => {
loading.value = true;
try {
console.log('loaded')
const result = await getRecurrentPayments();
recurrentPayments.value = result.data;
} catch (error) {

View File

@@ -21,7 +21,6 @@ const fetchCategories = async () => {
try {
const response = await getTransactions('INSTANT');
transactions.value = response.data
console.log(transactions.value)
} catch (error) {
console.error('Error fetching categories:', error);
}
@@ -72,7 +71,7 @@ onMounted(async () => {
</IconField>
<div class="mt-4">
<BudgetTransactionView class="mb-2" v-for="transaction in filteredTransactions" :transaction="transaction"/>
<BudgetTransactionView class="mb-2" v-for="transaction in filteredTransactions" :transaction="transaction" :is-list="true"/>
</div>
</div>
</div>

View File

@@ -8,11 +8,15 @@ import Aura from '@primevue/themes/aura';
import router from './router';
import Ripple from "primevue/ripple";
import ToastService from 'primevue/toastservice'
import Tooltip from 'primevue/tooltip';
const app = createApp(App);
app.use(router);
app.use(ToastService);
app.directive('ripple', Ripple);
app.directive('tooltip', Tooltip);
app.use(PrimeVue, {
theme: {
preset: Aura

View File

@@ -3,8 +3,8 @@ import axios from 'axios';
// Создание экземпляра axios с базовым URL
const apiClient = axios.create({
// baseURL: 'https://luminic.space/api/v1',
baseURL: 'http://localhost:8000/api/v1',
baseURL: 'https://luminic.space/api/v1',
// baseURL: 'http://localhost:8000/api/v1',
headers: {
'Content-Type': 'application/json',

View File

@@ -1,4 +1,4 @@
import {createRouter, createWebHistory, useRoute} from 'vue-router';
import {createRouter, createWebHistory} from 'vue-router';
import CategoriesList from '@/components/settings/categories/CategoriesList.vue';
import CreateCategoryModal from "@/components/settings/categories/CreateCategoryModal.vue";
import CategoryListItem from "@/components/settings/categories/CategoryListItem.vue"; // Импортируем новый компонент
@@ -10,18 +10,33 @@ import TransactionList from "@/components/transactions/TransactionList.vue";
import LoginView from "@/components/auth/LoginView.vue";
const routes = [
{path: '/', name: 'Budgets main', component: BudgetList, meta: { requiresAuth: true }},
{ path: '/login', component: LoginView },
{path: '/budgets', name: 'Budgets', component: BudgetList, meta: { requiresAuth: true }},
{path: '/budgets/:id', name: 'BudgetView', component: BudgetView, meta: { requiresAuth: true }},
{path: '/transactions/:mode*', name: 'Transaction List', component: TransactionList, meta: { requiresAuth: true }},
{path: '/login', component: LoginView},
{path: '/', name: 'Budgets main', component: BudgetList, meta: {requiresAuth: true}},
{path: '/analytics', name: 'Analytics', component: BudgetList, meta: {requiresAuth: true}},
{path: '/budgets', name: 'Budgets', component: BudgetList, meta: {requiresAuth: true}},
{path: '/budgets/:id', name: 'BudgetView', component: BudgetView, meta: {requiresAuth: true}},
{path: '/transactions/:mode*', name: 'Transaction List', component: TransactionList, meta: {requiresAuth: true}},
// {path: '/transactions/create', name: 'Transaction List', component: TransactionList},
{path: '/settings/', name: 'Settings', component: SettingsView, meta: { requiresAuth: true }},
{path: '/settings/categories', name: 'Categories', component: CategoriesList, meta: { requiresAuth: true }},
{path: '/settings/recurrents', name: 'Recurrent operations list', component: RecurrentList, meta: { requiresAuth: true }},
{path: '/settings/categories/create', name: "Categories Creation", component: CreateCategoryModal, meta: { requiresAuth: true }},// Добавляем новый маршрут
{path: '/settings/categories/one', name: "Categories Creation", component: CategoryListItem, meta: { requiresAuth: true }}// Добавляем новый маршрут
{path: '/settings/', name: 'Settings', component: SettingsView, meta: {requiresAuth: true}},
{path: '/settings/categories', name: 'Categories', component: CategoriesList, meta: {requiresAuth: true}},
{
path: '/settings/recurrents',
name: 'Recurrent operations list',
component: RecurrentList,
meta: {requiresAuth: true}
},
{
path: '/settings/categories/create',
name: "Categories Creation",
component: CreateCategoryModal,
meta: {requiresAuth: true}
},// Добавляем новый маршрут
{
path: '/settings/categories/one',
name: "Categories Creation",
component: CategoryListItem,
meta: {requiresAuth: true}
}// Добавляем новый маршрут
];
const router = createRouter({
@@ -32,11 +47,8 @@ const router = createRouter({
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token');
if (to.meta.requiresAuth && !token) {
const router= useRoute()
console.log(to)
console.log(router.path)
console.log(router.params)
next('/login?back='+to.fullPath);
// const router = useRoute()
next('/login?back=' + to.fullPath);
} else {
next();
}

View File

@@ -3,7 +3,7 @@ import {Budget, BudgetCategory} from "@/models/Budget";
// Импортируете настроенный экземпляр axios
export const getBudgetInfos = async () => {
console.log('getBudgetInfos');
let response = await apiClient.get('/budgets/');
let budgetInfos = response.data;
budgetInfos.forEach((budgetInfo: Budget) => {
@@ -24,22 +24,43 @@ export const getBudgetInfos = async () => {
return budgetInfos
}
export const getBudgetTransactions = async (budgetId, transactionType, categoryType) => {
let url = `/budgets/${budgetId}/transactions`
if (transactionType) {
url += '/' + transactionType
}
if (transactionType && categoryType) {
url += '/'+categoryType
}
// if (!categoryType) {
// throw new Error('No CategoryType');
// }
let response = await apiClient.get(url);
let transactions = response.data;
transactions.forEach(e => {
e.date = new Date(e.date)
})
return transactions
}
export const getBudgetCategories = async (budgetId) => {
let response = await apiClient.get('/budgets/' + budgetId + '/categories/');
return response.data;
}
export const getBudgetCategoriesSums = async (budgetId) => {
let response = await apiClient.get('/budgets/' + budgetId + '/categories/_calc_sums');
return response.data;
}
export const getBudgetInfo = async (budget_id: number) => {
console.log('getBudgetInfo');
let budgetInfo = await apiClient.get('/budgets/' + budget_id);
budgetInfo = budgetInfo.data;
budgetInfo.plannedExpenses.forEach(e => {
e.date = new Date(e.date)
})
budgetInfo.plannedIncomes.forEach(e => {
e.date = new Date(e.date)
})
budgetInfo.transactions.forEach(e => {
e.date = new Date(e.date)
})
budgetInfo.dateFrom = new Date(budgetInfo.dateFrom)
budgetInfo.dateTo = new Date(budgetInfo.dateTo)
return budgetInfo
};

View File

@@ -36,8 +36,13 @@ export const createTransactionRequest = async (transaction: Transaction) => {
export const updateTransactionRequest = async (transaction: Transaction) => {
const id = transaction.id
transaction.date = format(transaction.date, 'yyyy-MM-dd')
return await apiClient.put(`/transactions/${id}`, transaction);
// transaction.date = format(transaction.date, 'yyyy-MM-dd')
const response = await apiClient.put(`/transactions/${id}`, transaction);
transaction = response.data
transaction.date = new Date(transaction.date);
console.log(transaction.date);
return transaction
};
export const deleteTransactionRequest = async (id: number) => {

View File

@@ -4,7 +4,6 @@ export const formatAmount = (amount: number) => {
export const formatDate = (date) => {
const validDate = typeof date === 'string' ? new Date(date) : date;
// Проверяем, является ли validDate корректной датой
if (isNaN(validDate.getTime())) {
return 'Invalid Date'; // Если дата неверная, возвращаем текст ошибки