ver 2
This commit is contained in:
@@ -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 = {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,12 +17,43 @@ ChartJS.register(ChartDataLabels);
|
|||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const categoriesSums = ref([])
|
const categoriesSums = ref([])
|
||||||
|
const dataTableCategories = ref([])
|
||||||
|
|
||||||
|
|
||||||
const fetchCategoriesSums = async () => {
|
const fetchCategoriesSums = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
categoriesSums.value = await getTransactionCategoriesSums()
|
categoriesSums.value = await getTransactionCategoriesSums()
|
||||||
// console.log(categoriesSums.value)
|
// console.log(categoriesSums.value)
|
||||||
|
for (let category of categoriesSums.value) {
|
||||||
|
// console.log(category)
|
||||||
|
// [{
|
||||||
|
// "category": category[0],
|
||||||
|
// "category"+[category[1]]: category[2]
|
||||||
|
// }]
|
||||||
|
// console.log('test')
|
||||||
|
// console.log('dataTableCategories '+ dataTableCategories.value)
|
||||||
|
let categoryInList = dataTableCategories.value.find((listCategory: Category) => {
|
||||||
|
// console.log(listCategory['category'].id)
|
||||||
|
// console.log(category[0].id)
|
||||||
|
return listCategory['category'].id === category[0].id
|
||||||
|
})
|
||||||
|
console.log('cat in list ' + categoryInList)
|
||||||
|
if (categoryInList) {
|
||||||
|
console.log('cat[1] '+ category[1])
|
||||||
|
console.log('cat[2] '+ category[2])
|
||||||
|
categoryInList[category[1]] = category[2]
|
||||||
|
// console.log(categoryInList)
|
||||||
|
} else {
|
||||||
|
dataTableCategories.value.push({'category': category[0]})
|
||||||
|
dataTableCategories.value.filter((listCategory: Category) => {
|
||||||
|
return listCategory['category'].id === category.id
|
||||||
|
})[category[1]] = category[2]
|
||||||
|
}
|
||||||
|
// console.log(categoryInList)
|
||||||
|
}
|
||||||
|
// console.log(dataTableCategories.value)
|
||||||
|
// console.log(categoriesSums.value)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching categories sums:', error);
|
console.error('Error fetching categories sums:', error);
|
||||||
}
|
}
|
||||||
@@ -213,8 +244,13 @@ onMounted(async () => {
|
|||||||
placeholder="Выберите категории"
|
placeholder="Выберите категории"
|
||||||
:maxSelectedLabels="3" class="w-full md:w-80"/>
|
:maxSelectedLabels="3" class="w-full md:w-80"/>
|
||||||
|
|
||||||
<Chart v-if="selectedCategories.length > 0" type="line" :data="chartData" :options="chartOptions" class="h-[30rem]"/>
|
<Chart v-if="selectedCategories.length > 0" type="line" :data="chartData" :options="chartOptions"
|
||||||
|
class="h-[30rem]"/>
|
||||||
|
|
||||||
|
{{dataTableCategories}}
|
||||||
|
<DataTable :value="dataTableCategories">
|
||||||
|
<!-- <Column v-for="dataTableCategories"/>-->
|
||||||
|
</DataTable>
|
||||||
<!-- {{categories}}-->
|
<!-- {{categories}}-->
|
||||||
<!-- {{// chartData}}-->
|
<!-- {{// chartData}}-->
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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` при обновлении
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -5,23 +5,33 @@
|
|||||||
<div class="flex flex-row gap-4 items-center">
|
<div class="flex flex-row gap-4 items-center">
|
||||||
<h2 class="text-4xl font-bold">Бюджеты</h2>
|
<h2 class="text-4xl font-bold">Бюджеты</h2>
|
||||||
<Button label="+ Создать" @click="creationOpened=true" size="small"/>
|
<Button label="+ Создать" @click="creationOpened=true" size="small"/>
|
||||||
<BudgetCreationView :opened="creationOpened" @budget-created="creationSuccessShow()" @close-modal="creationOpened=false" />
|
<BudgetCreationView :opened="creationOpened" @budget-created="creationSuccessShow"
|
||||||
|
@close-modal="creationOpened=false"/>
|
||||||
<StatusView :show="creationSuccessModal" :is-error="false" :message="'Бюджет создан!'"/>
|
<StatusView :show="creationSuccessModal" :is-error="false" :message="'Бюджет создан!'"/>
|
||||||
</div>
|
</div>
|
||||||
<!-- Плитка с бюджетами -->
|
<!-- Плитка с бюджетами -->
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
<!-- Будущие и текущие бюджеты -->
|
<!-- Будущие и текущие бюджеты -->
|
||||||
|
<ConfirmDialog/>
|
||||||
|
<Toast/>
|
||||||
<div v-for="budget in budgetInfos" :key="budget.id" class="p-4 shadow-lg rounded-lg bg-white"
|
<div v-for="budget in budgetInfos" :key="budget.id" class="p-4 shadow-lg rounded-lg bg-white"
|
||||||
:class="budget.dateTo < new Date() ? 'bg-gray-100 opacity-60' : ''">
|
:class="budget.dateTo < new Date() ? 'bg-gray-100 opacity-60' : ''">
|
||||||
<div class="flex flex-row justify-between">
|
<div class="flex flex-row justify-between gap-4">
|
||||||
<div class="text-xl font-bold mb-2">{{ budget.name }}</div>
|
<div class="flex flex-col justify-between gap-5">
|
||||||
|
<div class="text-xl font-bold ">{{ budget.name }}</div>
|
||||||
|
<div class="text-sm text-gray-600 ">
|
||||||
|
{{ formatDate(budget.dateFrom) }} - {{ formatDate(budget.dateTo) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col justify-between gap-4">
|
||||||
<router-link :to="'/budgets/'+budget.id">
|
<router-link :to="'/budgets/'+budget.id">
|
||||||
<i class="pi pi-arrow-circle-right text-green-500" style="font-size: 1.5rem;"/>
|
<i class="pi pi-arrow-circle-right text-green-500" style="font-size: 1.5rem;"/>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<button @click="deleteBudget(budget)"><i class="pi pi-trash" style="color:red; font-size: 1.2rem"/></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-600 mb-4">
|
|
||||||
{{ formatDate(budget.dateFrom) }} - {{ formatDate(budget.dateTo) }}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <div class="mb-4">-->
|
<!-- <div class="mb-4">-->
|
||||||
<!-- <div class="text-sm">Total Income: <span class="font-bold">{{ formatAmount(budgettotalIncomes) }} ₽</span></div>-->
|
<!-- <div class="text-sm">Total Income: <span class="font-bold">{{ formatAmount(budgettotalIncomes) }} ₽</span></div>-->
|
||||||
<!-- <div class="text-sm">Total Expenses: <span class="font-bold">{{ formatAmount(budget.totalExpenses) }} ₽</span></div>-->
|
<!-- <div class="text-sm">Total Expenses: <span class="font-bold">{{ formatAmount(budget.totalExpenses) }} ₽</span></div>-->
|
||||||
@@ -59,27 +69,42 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {onMounted, ref} from 'vue';
|
import {onMounted, ref} from 'vue';
|
||||||
import {BudgetInfo} from "@/models/Budget";
|
import {Budget, BudgetInfo} from "@/models/Budget";
|
||||||
import {getBudgetInfos} from "@/services/budgetsService";
|
import {createBudget, deleteBudgetRequest, getBudgetInfos} from "@/services/budgetsService";
|
||||||
import {formatDate} from "@/utils/utils";
|
import {formatDate} from "@/utils/utils";
|
||||||
import LoadingView from "@/components/LoadingView.vue";
|
import LoadingView from "@/components/LoadingView.vue";
|
||||||
import Button from "primevue/button";
|
import Button from "primevue/button";
|
||||||
|
import ConfirmDialog from "primevue/confirmdialog";
|
||||||
import BudgetCreationView from "@/components/budgets/BudgetCreationView.vue";
|
import BudgetCreationView from "@/components/budgets/BudgetCreationView.vue";
|
||||||
import StatusView from "@/components/StatusView.vue";
|
import StatusView from "@/components/StatusView.vue";
|
||||||
|
import {useConfirm} from "primevue/useconfirm";
|
||||||
|
import {useToast} from "primevue/usetoast";
|
||||||
|
import Toast from "primevue/toast";
|
||||||
|
|
||||||
|
const confirm = useConfirm();
|
||||||
|
const toast = useToast();
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const budgetInfos = ref<BudgetInfo[]>([])
|
const budgetInfos = ref<BudgetInfo[]>([])
|
||||||
const creationOpened = ref(false)
|
const creationOpened = ref(false)
|
||||||
const creationSuccessModal = ref(false)
|
const creationSuccessModal = ref(false)
|
||||||
const creationSuccessShow = async () => {
|
|
||||||
creationOpened.value = false
|
const creationSuccessShow = async (budget, createRecurrentPayments) => {
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
await createBudget(budget, createRecurrentPayments)
|
||||||
budgetInfos.value = await getBudgetInfos()
|
budgetInfos.value = await getBudgetInfos()
|
||||||
creationSuccessModal.value = true
|
toast.add({severity: 'success', summary: 'Успешно!', detail: 'Бюджет создан!', life: 3000});
|
||||||
setTimeout(() => {
|
creationOpened.value = false
|
||||||
creationSuccessModal.value = false
|
} catch (error) {
|
||||||
|
console.log(error.response.data["message"])
|
||||||
|
toast.add({severity: "error", summary: "Бюджет не создан", detail: error.response.data["message"], life: 3000});
|
||||||
}
|
}
|
||||||
, 1000)
|
// creationSuccessModal.value = true
|
||||||
|
// setTimeout(() => {
|
||||||
|
// creationSuccessModal.value = false
|
||||||
|
// }
|
||||||
|
// , 1000)
|
||||||
}
|
}
|
||||||
const pastBudgets = ref([
|
const pastBudgets = ref([
|
||||||
{
|
{
|
||||||
@@ -106,6 +131,37 @@ const pastBudgets = ref([
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const deleteBudget = async (budget: Budget) => {
|
||||||
|
|
||||||
|
confirm.require({
|
||||||
|
message: `Вы действительно хотите удалить бюджет ${budget.name} ?`,
|
||||||
|
header: 'Удаление бюджета',
|
||||||
|
icon: 'pi pi-info-circle',
|
||||||
|
rejectLabel: 'Отмена',
|
||||||
|
rejectProps: {
|
||||||
|
label: 'Отмена',
|
||||||
|
severity: 'secondary',
|
||||||
|
outlined: true
|
||||||
|
},
|
||||||
|
acceptProps: {
|
||||||
|
label: 'Удалить',
|
||||||
|
severity: 'danger'
|
||||||
|
},
|
||||||
|
accept: async () => {
|
||||||
|
try {
|
||||||
|
await deleteBudgetRequest(budget.id)
|
||||||
|
// showToast("Confirmed")
|
||||||
|
budgetInfos.value = await getBudgetInfos()
|
||||||
|
toast.add({severity: 'success', summary: 'Успешно!', detail: 'Бюджет удален!', life: 3000});
|
||||||
|
} catch (e: Error) {
|
||||||
|
toast.add({severity: 'error', summary: "Ошибка при удалении", detail: e.message, life: 3000});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reject: () => {
|
||||||
|
toast.add({severity: 'info', summary: 'Отменено', detail: 'Вы отменили удаление', life: 3000});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ import {computed, onMounted, PropType, ref} from "vue";
|
|||||||
import {Transaction} from "@/models/Transaction";
|
import {Transaction} from "@/models/Transaction";
|
||||||
import {Category, CategoryType} from "@/models/Category";
|
import {Category, CategoryType} from "@/models/Category";
|
||||||
import {getCategories, getCategoryTypes} from "@/services/categoryService";
|
import {getCategories, getCategoryTypes} from "@/services/categoryService";
|
||||||
import {setTransactionDoneRequest} from "@/services/transactionService";
|
import { updateTransactionRequest} from "@/services/transactionService";
|
||||||
import {formatAmount, formatDate} from "@/utils/utils";
|
import {formatAmount, formatDate} from "@/utils/utils";
|
||||||
import TransactionForm from "@/components/transactions/TransactionForm.vue";
|
import TransactionForm from "@/components/transactions/TransactionForm.vue";
|
||||||
|
import {useToast} from "primevue/usetoast";
|
||||||
|
|
||||||
|
|
||||||
const props = defineProps(
|
const props = defineProps(
|
||||||
@@ -30,31 +31,44 @@ const props = defineProps(
|
|||||||
const emits = defineEmits(['open-drawer', 'transaction-checked', 'transaction-updated', 'delete-transaction'])
|
const emits = defineEmits(['open-drawer', 'transaction-checked', 'transaction-updated', 'delete-transaction'])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const setIsDoneTrue = async () => {
|
const setIsDoneTrue = async () => {
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
await setTransactionDoneRequest(props.transaction)
|
console.log("here")
|
||||||
emits('transaction-checked')
|
await updateTransactionRequest(props.transaction)
|
||||||
}, 10);
|
emits('transaction-updated')
|
||||||
|
}, 20);
|
||||||
// showedTransaction.value.isDone = !showedTransaction.value.isDone;
|
// showedTransaction.value.isDone = !showedTransaction.value.isDone;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
const drawerOpened = ref(false)
|
const drawerOpened = ref(false)
|
||||||
const toggleDrawer = () => {
|
const toggleDrawer = () => {
|
||||||
|
if (props.transaction?.parentId) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'warn',
|
||||||
|
summary: 'Транзакцию нельзя изменить!',
|
||||||
|
detail: 'Транзакции созданные из плана не могут быть изменены.',
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
if (drawerOpened.value) {
|
if (drawerOpened.value) {
|
||||||
drawerOpened.value = false;
|
drawerOpened.value = false;
|
||||||
}
|
}
|
||||||
drawerOpened.value = !drawerOpened.value
|
drawerOpened.value = !drawerOpened.value
|
||||||
emits('open-drawer', props.transaction)
|
emits('open-drawer', props.transaction)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const transactionUpdate = () => {
|
const transactionUpdate = () => {
|
||||||
|
console.log("transaction updated")
|
||||||
emits('transaction-updated')
|
emits('transaction-updated')
|
||||||
}
|
}
|
||||||
|
|
||||||
const isPlanned = computed(() => {
|
const isPlanned = computed(() => {
|
||||||
return props.transaction?.transactionType.code === "PLANNED"
|
return props.transaction?.type.code === "PLANNED"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -94,6 +108,8 @@ const closeDrawer = () => {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// await fetchCategories();
|
// await fetchCategories();
|
||||||
// await fetchCategoryTypes()
|
// await fetchCategoryTypes()
|
||||||
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -104,12 +120,15 @@ onMounted(async () => {
|
|||||||
"
|
"
|
||||||
class="flex bg-white min-w-fit max-h-fit flex-row items-center gap-4 w-full ">
|
class="flex bg-white min-w-fit max-h-fit flex-row items-center gap-4 w-full ">
|
||||||
<div>
|
<div>
|
||||||
<p v-if="transaction.transactionType.code=='INSTANT' || props.isList"
|
<p v-if="transaction.type.code=='INSTANT' || props.isList"
|
||||||
class="text-6xl font-bold text-gray-700 dark:text-gray-400">
|
class="text-6xl font-bold text-gray-700 dark:text-gray-400">
|
||||||
{{ transaction.category.icon }}</p>
|
{{ transaction.category.icon }}
|
||||||
<Checkbox v-model="transaction.isDone" v-else-if="transaction.transactionType.code=='PLANNED' && !props.isList"
|
</p>
|
||||||
|
|
||||||
|
<Checkbox v-model="transaction.isDone" v-else-if="transaction.type.code=='PLANNED' && !props.isList"
|
||||||
:binary="true"
|
:binary="true"
|
||||||
@click="setIsDoneTrue"/>
|
@click="setIsDoneTrue"/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<button class="flex flex-row items-center p-x-4 justify-between w-full " @click="toggleDrawer">
|
<button class="flex flex-row items-center p-x-4 justify-between w-full " @click="toggleDrawer">
|
||||||
|
|
||||||
|
|||||||
@@ -15,13 +15,46 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div :class="!updateLoading ? '' : 'h-fit bg-white opacity-50 z-0 '" class=" flex flex-col gap-3">
|
<div :class="!updateLoading ? '' : 'h-fit bg-white opacity-50 z-0 '" class=" flex flex-col gap-3">
|
||||||
<div class="flex flex-col ">
|
<div class="flex flex-row justify-between ">
|
||||||
<!-- {{ budget }}-->
|
<div class="flex flex-col gap-2">
|
||||||
<h2 class="text-4xl font-bold">Бюджет {{ budget.name }} </h2>
|
<h2 class="text-4xl font-bold">Бюджет {{ budget.name }} </h2>
|
||||||
<div class="flex flex-row gap-2 text-xl">{{ formatDate(budget.dateFrom) }} -
|
<div class="flex flex-row gap-2 text-xl">{{ formatDate(budget.dateFrom) }} -
|
||||||
{{ formatDate(budget.dateTo) }}
|
{{ formatDate(budget.dateTo) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2 justify-center">
|
||||||
|
<button class="flex flex-row bg-white py-6 px-4 shadow-lg rounded-lg items-center h-6 justify-center gap-2"
|
||||||
|
@click="warnsOpened = !warnsOpened">
|
||||||
|
<span class="bg-gray-300 p-1 rounded font-bold">{{
|
||||||
|
warns ? warns.length : 0
|
||||||
|
}}</span><span>Уведомлений</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div v-if="warnsOpened"
|
||||||
|
class="absolute h-fit max-h-128 w-128 overflow-auto bg-white shadow-lg rounded-lg top-32 right-4 z-50">
|
||||||
|
<div v-if="checkWarnsExists" class="flex flex-col p-4">
|
||||||
|
<div v-for="warn in warns">
|
||||||
|
<div class="flex flex-row items-center gap-2 justify-between">
|
||||||
|
|
||||||
|
<div class="flex flex-row items-center gap-2">
|
||||||
|
<div class="text-2xl">{{ warn.message.icon }}</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="font-bold">{{ warn.message.title }}</span>
|
||||||
|
<span v-html="warn.message.body"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button @click="hideWarn(warn.id)"><i class="pi pi-times" style="font-size: 0.7rem"></i></button>
|
||||||
|
</div>
|
||||||
|
<Divider/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex items-center justify-center p-16">
|
||||||
|
<button @click="fetchWarns(true)">Показать скрытые</button></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<!-- Аналитика и плановые доходы/расходы -->
|
<!-- Аналитика и плановые доходы/расходы -->
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4 items-start ">
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4 items-start ">
|
||||||
@@ -58,6 +91,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col items-center ">
|
<div class="flex flex-col items-center ">
|
||||||
<h4 class="text-lg font-bold ">Расходы</h4>
|
<h4 class="text-lg font-bold ">Расходы</h4>
|
||||||
|
|
||||||
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center"
|
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center"
|
||||||
:class="totalExpenses > totalIncomes ? ' text-red-700' : ''">
|
:class="totalExpenses > totalIncomes ? ' text-red-700' : ''">
|
||||||
-{{ formatAmount(totalExpenses) }} ({{ formatAmount(totalExpenses - totalIncomes) }})
|
-{{ formatAmount(totalExpenses) }} ({{ formatAmount(totalExpenses - totalIncomes) }})
|
||||||
@@ -65,6 +99,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 !gap-1 mt-4" :class="detailedShowed ? 'block' : 'hidden'">
|
<div class="grid grid-cols-2 !gap-1 mt-4" :class="detailedShowed ? 'block' : 'hidden'">
|
||||||
<div class="flex flex-col items-center font-bold ">
|
<div class="flex flex-col items-center font-bold ">
|
||||||
<p class="font-light ">в первый период</p>
|
<p class="font-light ">в первый период</p>
|
||||||
@@ -170,8 +206,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class=" flex gap-2 overflow-x-auto ">
|
<div class=" flex gap-2 overflow-x-auto ">
|
||||||
|
|
||||||
<button v-for="categorySum in transactionCategoriesSums" @click="selectCategoryType(categorySum.category.id)"
|
<button v-for="categorySum in transactionCategoriesSums"
|
||||||
class="rounded-xl border p-1 bg-white border-gray-300 mb-2 min-w-fit px-2" :class="selectedCategoryId == categorySum.category.id ? '!bg-blue-100' : ''">
|
@click="selectCategoryType(categorySum.category.id)"
|
||||||
|
class="rounded-xl border p-1 bg-white border-gray-300 mb-2 min-w-fit px-2"
|
||||||
|
:class="selectedCategoryId == categorySum.category.id ? '!bg-blue-100' : ''">
|
||||||
<p><span class="text-sm font-bold">{{ categorySum.category.name }}</span>: {{ categorySum.sum }} ₽</p>
|
<p><span class="text-sm font-bold">{{ categorySum.category.name }}</span>: {{ categorySum.sum }} ₽</p>
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
@@ -181,6 +219,7 @@
|
|||||||
:transaction="transaction"
|
:transaction="transaction"
|
||||||
:is-list="true"
|
:is-list="true"
|
||||||
@transaction-updated="updateTransactions"
|
@transaction-updated="updateTransactions"
|
||||||
|
@transaction-checked="updateTransactions"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -211,8 +250,9 @@
|
|||||||
<ul class="space-y-2">
|
<ul class="space-y-2">
|
||||||
<!-- {{ plannedIncomes }}-->
|
<!-- {{ plannedIncomes }}-->
|
||||||
<BudgetTransactionView v-for="transaction in plannedIncomes" :transaction="transaction"
|
<BudgetTransactionView v-for="transaction in plannedIncomes" :transaction="transaction"
|
||||||
:is-list="false" @transaction-checked="fetchBudgetTransactions"
|
:is-list="false"
|
||||||
@transaction-updated="updateTransactions"/>
|
@transaction-updated="updateTransactions"
|
||||||
|
@transaction-checked="updateTransactions"/>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
@@ -241,7 +281,7 @@
|
|||||||
<ul class="space-y-2">
|
<ul class="space-y-2">
|
||||||
|
|
||||||
<BudgetTransactionView v-for="transaction in plannedExpenses" :transaction="transaction"
|
<BudgetTransactionView v-for="transaction in plannedExpenses" :transaction="transaction"
|
||||||
:is-list="false" @transaction-checked="fetchBudgetTransactions"
|
:is-list="false" @transaction-checked="updateTransactions"
|
||||||
@transaction-updated="updateTransactions"/>
|
@transaction-updated="updateTransactions"/>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -268,8 +308,10 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class=" flex gap-2 overflow-x-auto">
|
<div class=" flex gap-2 overflow-x-auto">
|
||||||
<button v-for="categorySum in transactionCategoriesSums" @click="selectCategoryType(categorySum.category.id)"
|
<button v-for="categorySum in transactionCategoriesSums"
|
||||||
class="rounded-xl border p-1 bg-white border-gray-300 mb-2 min-w-fit px-2" :class="selectedCategoryId == categorySum.category.id ? '!bg-blue-100' : ''">
|
@click="selectCategoryType(categorySum.category.id)"
|
||||||
|
class="rounded-xl border p-1 bg-white border-gray-300 mb-2 min-w-fit px-2"
|
||||||
|
:class="selectedCategoryId == categorySum.category.id ? '!bg-blue-100' : ''">
|
||||||
<p><span class="text-sm font-bold">{{ categorySum.category.name }}</span>: {{ categorySum.sum }} ₽</p>
|
<p><span class="text-sm font-bold">{{ categorySum.category.name }}</span>: {{ categorySum.sum }} ₽</p>
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
@@ -280,6 +322,7 @@
|
|||||||
:is-list="true"
|
:is-list="true"
|
||||||
@transaction-updated="updateTransactions"
|
@transaction-updated="updateTransactions"
|
||||||
|
|
||||||
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -316,9 +359,11 @@ import {
|
|||||||
getBudgetCategories,
|
getBudgetCategories,
|
||||||
getBudgetInfo,
|
getBudgetInfo,
|
||||||
getBudgetTransactions,
|
getBudgetTransactions,
|
||||||
updateBudgetCategoryRequest
|
updateBudgetCategoryRequest,
|
||||||
|
getWarns,
|
||||||
|
hideWarnRequest
|
||||||
} from "@/services/budgetsService";
|
} from "@/services/budgetsService";
|
||||||
import {Budget, BudgetCategory, BudgetInfo} from "@/models/Budget";
|
import {Budget, BudgetCategory, Warn} from "@/models/Budget";
|
||||||
import {useRoute} from "vue-router";
|
import {useRoute} from "vue-router";
|
||||||
import {formatAmount, formatDate} from "@/utils/utils";
|
import {formatAmount, formatDate} from "@/utils/utils";
|
||||||
import ProgressBar from "primevue/progressbar";
|
import ProgressBar from "primevue/progressbar";
|
||||||
@@ -331,6 +376,7 @@ import LoadingView from "@/components/LoadingView.vue";
|
|||||||
import ChartDataLabels from 'chartjs-plugin-datalabels';
|
import ChartDataLabels from 'chartjs-plugin-datalabels';
|
||||||
import {Chart as ChartJS} from 'chart.js/auto';
|
import {Chart as ChartJS} from 'chart.js/auto';
|
||||||
import SelectButton from "primevue/selectbutton";
|
import SelectButton from "primevue/selectbutton";
|
||||||
|
import Divider from "primevue/divider";
|
||||||
import TransactionForm from "@/components/transactions/TransactionForm.vue";
|
import TransactionForm from "@/components/transactions/TransactionForm.vue";
|
||||||
|
|
||||||
// Зарегистрируем плагин
|
// Зарегистрируем плагин
|
||||||
@@ -350,10 +396,24 @@ const modes = [
|
|||||||
|
|
||||||
const value = ref(50)
|
const value = ref(50)
|
||||||
|
|
||||||
|
const warnsOpened = ref(false);
|
||||||
|
|
||||||
|
const hideWarn = async (warnId: string) => {
|
||||||
|
await hideWarnRequest(route.params.id, warnId)
|
||||||
|
await fetchWarns()
|
||||||
|
}
|
||||||
|
|
||||||
const leftForUnplanned = ref(0)
|
const leftForUnplanned = ref(0)
|
||||||
|
|
||||||
|
|
||||||
const budget = ref<Budget>()
|
const budget = ref<Budget>()
|
||||||
|
const warns = ref<[Warn]>()
|
||||||
|
const checkWarnsExists = computed(() => {
|
||||||
|
console.log(warns?.value && warns.value.length > 0 ? "true" : "false");
|
||||||
|
return warns?.value?.length > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
const plannedIncomes = ref<Transaction[]>([])
|
const plannedIncomes = ref<Transaction[]>([])
|
||||||
const totalIncomes = computed(() => {
|
const totalIncomes = computed(() => {
|
||||||
let totalIncome = 0;
|
let totalIncome = 0;
|
||||||
@@ -365,7 +425,7 @@ const totalIncomes = computed(() => {
|
|||||||
|
|
||||||
const totalInstantIncomes = computed(() => {
|
const totalInstantIncomes = computed(() => {
|
||||||
let totalIncome = 0;
|
let totalIncome = 0;
|
||||||
transactions.value.filter(t => t.transactionType.code=='INSTANT' && t.category.type.code =='INCOME' ).forEach((i) => {
|
transactions.value.filter(t => t.type.code == 'INSTANT' && t.category.type.code == 'INCOME').forEach((i) => {
|
||||||
totalIncome += i.amount
|
totalIncome += i.amount
|
||||||
})
|
})
|
||||||
return totalIncome
|
return totalIncome
|
||||||
@@ -383,7 +443,7 @@ const totalIncomeLeftToGet = computed(() => {
|
|||||||
|
|
||||||
const totalLoans = computed(() => {
|
const totalLoans = computed(() => {
|
||||||
let value = 0
|
let value = 0
|
||||||
categories.value.filter((cat) => cat.category.id == 29).forEach(cat => {
|
categories.value.filter((cat) => cat.category.id == "675850148198643f121e465d").forEach(cat => {
|
||||||
value += cat.currentLimit
|
value += cat.currentLimit
|
||||||
})
|
})
|
||||||
return value
|
return value
|
||||||
@@ -402,7 +462,7 @@ const savingRatio = computed(() => {
|
|||||||
|
|
||||||
const totalSaving = computed(() => {
|
const totalSaving = computed(() => {
|
||||||
let value = 0
|
let value = 0
|
||||||
categories.value.filter((cat) => cat.category.id == 35).forEach(cat => {
|
categories.value.filter((cat) => cat.category.id == "675850148198643f121e466a").forEach(cat => {
|
||||||
value += cat.currentLimit
|
value += cat.currentLimit
|
||||||
})
|
})
|
||||||
return value
|
return value
|
||||||
@@ -441,13 +501,13 @@ const plannedExpenses = ref<Transaction[]>([])
|
|||||||
const totalExpenses = computed(() => {
|
const totalExpenses = computed(() => {
|
||||||
let totalExpense = 0;
|
let totalExpense = 0;
|
||||||
categories.value.forEach((cat) => {
|
categories.value.forEach((cat) => {
|
||||||
let catValue = cat.currentLimit - cat.categoryPlannedLimit
|
let catValue = cat.currentLimit
|
||||||
|
|
||||||
plannedExpenses.value.filter(t => t.category.id == cat.category.id).forEach((i) => {
|
|
||||||
|
|
||||||
catValue += i.amount
|
|
||||||
})
|
|
||||||
|
|
||||||
|
// plannedExpenses.value.filter(t => t.category.id == cat.category.id).forEach((i) => {
|
||||||
|
//
|
||||||
|
// catValue += i.amount
|
||||||
|
// })
|
||||||
|
//
|
||||||
totalExpense += catValue
|
totalExpense += catValue
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -465,7 +525,7 @@ const totalPlannedExpenses = computed(() => {
|
|||||||
|
|
||||||
const totalInstantExpenses = computed(() => {
|
const totalInstantExpenses = computed(() => {
|
||||||
let totalExpenses = 0;
|
let totalExpenses = 0;
|
||||||
transactions.value.filter(t => t.transactionType.code=='INSTANT' && t.category.type.code =='EXPENSE').forEach((i) => {
|
transactions.value.filter(t => t.type.code == 'INSTANT' && t.category.type.code == 'EXPENSE').forEach((i) => {
|
||||||
totalExpenses += i.amount
|
totalExpenses += i.amount
|
||||||
})
|
})
|
||||||
return totalExpenses
|
return totalExpenses
|
||||||
@@ -502,7 +562,12 @@ const fetchBudgetTransactions = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updateTransactions = async () => {
|
const updateTransactions = async () => {
|
||||||
await Promise.all([fetchPlannedIncomes(), fetchPlannedExpenses(), fetchBudgetCategories(), fetchBudgetTransactions()])
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
await Promise.all([fetchBudgetInfo(),fetchWarns()])
|
||||||
|
|
||||||
|
}, 10)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const categories = ref<BudgetCategory[]>([])
|
const categories = ref<BudgetCategory[]>([])
|
||||||
@@ -541,18 +606,27 @@ const transactionCategoriesSums = computed(() => {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
||||||
const budgetInfo = ref<BudgetInfo>();
|
const budgetInfo = ref<Budget>();
|
||||||
const fetchBudgetInfo = async () => {
|
const fetchBudgetInfo = async () => {
|
||||||
|
|
||||||
|
|
||||||
budget.value = await getBudgetInfo(route.params.id);
|
budget.value = await getBudgetInfo(route.params.id);
|
||||||
|
plannedExpenses.value = budget.value?.plannedExpenses.copyWithin()
|
||||||
|
plannedIncomes.value = budget.value?.plannedIncomes.copyWithin()
|
||||||
|
transactions.value = budget.value?.transactions.copyWithin()
|
||||||
|
categories.value = budget.value?.categories
|
||||||
updateLoading.value = false
|
updateLoading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const updateBudgetCategory = async (category) => {
|
const updateBudgetCategory = async (category) => {
|
||||||
|
|
||||||
// loading.value = true
|
// loading.value = true
|
||||||
await updateBudgetCategoryRequest(budget.value.id, category)
|
category = await updateBudgetCategoryRequest(budget.value.id, category)
|
||||||
|
await fetchBudgetInfo()
|
||||||
|
setTimeout(async () => {
|
||||||
|
await fetchWarns()
|
||||||
|
}, 500)
|
||||||
|
|
||||||
// categories.value = await getBudgetCategories(route.params.id)
|
// categories.value = await getBudgetCategories(route.params.id)
|
||||||
|
|
||||||
@@ -590,6 +664,7 @@ const incomesByPeriod = computed(() => {
|
|||||||
let incomesUntil25 = 0
|
let incomesUntil25 = 0
|
||||||
let incomesFrom25 = 0
|
let incomesFrom25 = 0
|
||||||
plannedIncomes.value.forEach((i) => {
|
plannedIncomes.value.forEach((i) => {
|
||||||
|
i.date = new Date(i.date)
|
||||||
|
|
||||||
if (i.date >= budget.value?.dateFrom && i.date <= twentyFour.value) {
|
if (i.date >= budget.value?.dateFrom && i.date <= twentyFour.value) {
|
||||||
|
|
||||||
@@ -606,17 +681,19 @@ const incomesByPeriod = computed(() => {
|
|||||||
const expensesByPeriod = computed(() => {
|
const expensesByPeriod = computed(() => {
|
||||||
let expensesUntil25 = 0
|
let expensesUntil25 = 0
|
||||||
let expensesFrom25 = 0
|
let expensesFrom25 = 0
|
||||||
|
let totalPlannedExpensesSum = 0
|
||||||
plannedExpenses.value.forEach((i) => {
|
plannedExpenses.value.forEach((i) => {
|
||||||
|
i.date = new Date(i.date)
|
||||||
if (i.date >= budget.value?.dateFrom && i.date <= twentyFour.value) {
|
if (i.date >= budget.value?.dateFrom && i.date <= twentyFour.value) {
|
||||||
expensesUntil25 += i.amount
|
expensesUntil25 += i.amount
|
||||||
} else {
|
} else {
|
||||||
expensesFrom25 += i.amount
|
expensesFrom25 += i.amount
|
||||||
}
|
}
|
||||||
|
totalPlannedExpensesSum += i.amount
|
||||||
})
|
})
|
||||||
categories.value.forEach((i) => {
|
categories.value.forEach((i) => {
|
||||||
expensesUntil25 += (i.currentLimit - i.categoryPlannedLimit) / 2
|
expensesUntil25 += (i.currentLimit - i.currentPlanned) / 2
|
||||||
expensesFrom25 += (i.currentLimit - i.categoryPlannedLimit) / 2
|
expensesFrom25 += (i.currentLimit - i.currentPlanned) / 2
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -760,7 +837,11 @@ const incomeExpenseChartOptions = ref({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
|
const fetchWarns = async (hidden: Boolean = null) => {
|
||||||
|
warns.value = await getWarns(route.params.id, hidden)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -768,11 +849,12 @@ onMounted(async () => {
|
|||||||
try {
|
try {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
fetchBudgetInfo(),
|
fetchBudgetInfo(),
|
||||||
|
fetchWarns()
|
||||||
// budget.value = await getBudgetInfo(route.params.id),
|
// budget.value = await getBudgetInfo(route.params.id),
|
||||||
fetchPlannedIncomes(),
|
// fetchPlannedIncomes(),
|
||||||
fetchPlannedExpenses(),
|
// fetchPlannedExpenses(),
|
||||||
fetchBudgetCategories(),
|
// fetchBudgetCategories(),
|
||||||
fetchBudgetTransactions(),
|
// fetchBudgetTransactions(),
|
||||||
]);
|
]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during fetching data:', error);
|
console.error('Error during fetching data:', error);
|
||||||
@@ -795,7 +877,6 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.max-h-tlist {
|
.max-h-tlist {
|
||||||
max-height: 1170px; /* Ограничение высоты списка */
|
max-height: 1170px; /* Ограничение высоты списка */
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 ">
|
||||||
|
|||||||
@@ -3,17 +3,18 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex flex-col bg-gray-100 dark:bg-gray-800 h-screen p-4">
|
<div v-else class="flex flex-col bg-gray-100 dark:bg-gray-800 h-screen p-4">
|
||||||
|
<ConfirmDialog/>
|
||||||
<!-- Заголовок и кнопка добавления категории -->
|
<!-- Заголовок и кнопка добавления категории -->
|
||||||
<div class="flex flex-row justify-between bg-gray-100">
|
<div class="flex flex-row justify-between bg-gray-100">
|
||||||
<h2 class="text-5xl font-bold">Categories</h2>
|
<h2 class="text-5xl font-bold">Категории</h2>
|
||||||
<Button label="Add Category" icon="pi pi-plus" class="p-button-success" @click="openCreateDialog(null)"/>
|
<Button label="Добавить категорию" icon="pi pi-plus" @click="openCreateDialog(null)"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Поле для поиска -->
|
<!-- Поле для поиска -->
|
||||||
<div class="my-4 w-full">
|
<div class="my-4 w-full">
|
||||||
<span class="p-input-icon-left flex flex-row gap-2 items-center ">
|
<span class="p-input-icon-left flex flex-row gap-2 items-center ">
|
||||||
<i class="pi pi-search"></i>
|
<i class="pi pi-search"></i>
|
||||||
<InputText v-model="searchTerm" placeholder="Search categories..." class="w-full"/>
|
<InputText v-model="searchTerm" placeholder="Поиск категорий..." class="w-full"/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@
|
|||||||
aria-labelledby="category-switch"/>
|
aria-labelledby="category-switch"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Список категорий с прокруткой для больших экранов -->
|
<!-- Список категорий с прокруткой для больших экранов -->
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="hidden sm:grid grid-cols-2 gap-x-10 w-full h-full justify-items-center overflow-y-auto">
|
<div class="hidden sm:grid grid-cols-2 gap-x-10 w-full h-full justify-items-center overflow-y-auto">
|
||||||
@@ -30,7 +32,7 @@
|
|||||||
<div class="grid h-full w-full min-w-fit overflow-y-auto">
|
<div class="grid h-full w-full min-w-fit overflow-y-auto">
|
||||||
<div class=" gap-4 ">
|
<div class=" gap-4 ">
|
||||||
<div class="flex flex-row gap-2 ">
|
<div class="flex flex-row gap-2 ">
|
||||||
<h3 class="text-2xl">Income Categories</h3>
|
<h3 class="text-2xl">Поступления</h3>
|
||||||
<Button icon="pi pi-plus" rounded outlined class="p-button-success" @click="openCreateDialog('INCOME')"/>
|
<Button icon="pi pi-plus" rounded outlined class="p-button-success" @click="openCreateDialog('INCOME')"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -42,7 +44,7 @@
|
|||||||
:category="category"
|
:category="category"
|
||||||
v-bind="category"
|
v-bind="category"
|
||||||
@open-edit="openEdit"
|
@open-edit="openEdit"
|
||||||
@delete-category="deleteCat"
|
@delete-category="confirmDelete"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -51,7 +53,7 @@
|
|||||||
<div class="grid h-full w-full min-w-fit overflow-y-auto">
|
<div class="grid h-full w-full min-w-fit overflow-y-auto">
|
||||||
<div class=" gap-4 justify-between ">
|
<div class=" gap-4 justify-between ">
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<h3 class="text-2xl">Expense Categories</h3>
|
<h3 class="text-2xl">Расходы</h3>
|
||||||
<Button icon="pi pi-plus" rounded outlined class="p-button-success !hover:bg-green-600"
|
<Button icon="pi pi-plus" rounded outlined class="p-button-success !hover:bg-green-600"
|
||||||
@click="openCreateDialog('EXPENSE')"/>
|
@click="openCreateDialog('EXPENSE')"/>
|
||||||
</div>
|
</div>
|
||||||
@@ -63,7 +65,7 @@
|
|||||||
:category="category"
|
:category="category"
|
||||||
v-bind="category"
|
v-bind="category"
|
||||||
@open-edit="openEdit"
|
@open-edit="openEdit"
|
||||||
@delete-category="deleteCat"
|
@delete-category="confirmDelete"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,7 +81,7 @@
|
|||||||
v-bind="category"
|
v-bind="category"
|
||||||
class="mt-2"
|
class="mt-2"
|
||||||
@open-edit="openEdit"
|
@open-edit="openEdit"
|
||||||
@delete-category="deleteCat"
|
@delete-category="confirmDelete"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -93,13 +95,15 @@
|
|||||||
@update:visible="closeCreateDialog"
|
@update:visible="closeCreateDialog"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import {computed, nextTick, onMounted, ref, watch} from 'vue';
|
import {computed, nextTick, onMounted, ref, watch} from 'vue';
|
||||||
import Button from 'primevue/button';
|
import Button from 'primevue/button';
|
||||||
import InputText from 'primevue/inputtext';
|
import InputText from 'primevue/inputtext';
|
||||||
import SelectButton from 'primevue/selectbutton';
|
import SelectButton from 'primevue/selectbutton';
|
||||||
|
import ConfirmDialog from "primevue/confirmdialog";
|
||||||
import CreateCategoryModal from './CreateCategoryModal.vue';
|
import CreateCategoryModal from './CreateCategoryModal.vue';
|
||||||
import CategoryListItem from '@/components/settings/categories/CategoryListItem.vue';
|
import CategoryListItem from '@/components/settings/categories/CategoryListItem.vue';
|
||||||
import {Category, CategoryType} from '@/models/Category';
|
import {Category, CategoryType} from '@/models/Category';
|
||||||
@@ -111,21 +115,18 @@ import {
|
|||||||
updateCategory
|
updateCategory
|
||||||
} from "@/services/categoryService";
|
} from "@/services/categoryService";
|
||||||
|
|
||||||
export default {
|
import {useConfirm} from "primevue/useconfirm";
|
||||||
components: {
|
import {useToast} from "primevue/usetoast";
|
||||||
Button,
|
|
||||||
InputText,
|
|
||||||
SelectButton,
|
|
||||||
CreateCategoryModal,
|
|
||||||
CategoryListItem,
|
|
||||||
},
|
|
||||||
setup() {
|
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const entireCategories = ref<Category[]>([]);
|
const entireCategories = ref<Category[]>([]);
|
||||||
const expenseCategories = ref<Category[]>([]);
|
const expenseCategories = ref<Category[]>([]);
|
||||||
const incomeCategories = ref<Category[]>([]);
|
const incomeCategories = ref<Category[]>([]);
|
||||||
const editingCategory = ref<Category | null>(null);
|
const editingCategory = ref<Category | null>(null);
|
||||||
const isDialogVisible = ref(false);
|
const isDialogVisible = ref(false);
|
||||||
|
const confirm = useConfirm();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
const fetchCategories = async () => {
|
const fetchCategories = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@@ -202,7 +203,36 @@ export default {
|
|||||||
closeCreateDialog();
|
closeCreateDialog();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const confirmDelete = async (category: Category) => {
|
||||||
|
|
||||||
|
confirm.require({
|
||||||
|
message: 'Вы уверены, что хотите выполнить это действие?\n Это нельзя будет отменить.\nВсе транзакции данной категории будут перенесены в категорию "Другое".\n',
|
||||||
|
header: `Удаление категории ${category.name}`,
|
||||||
|
icon: 'pi pi-info-circle',
|
||||||
|
rejectLabel: 'Cancel',
|
||||||
|
rejectProps: {
|
||||||
|
label: 'Cancel',
|
||||||
|
severity: 'secondary',
|
||||||
|
outlined: true
|
||||||
|
},
|
||||||
|
acceptProps: {
|
||||||
|
label: 'Delete',
|
||||||
|
severity: 'danger'
|
||||||
|
},
|
||||||
|
accept: async () => {
|
||||||
|
await deleteCategory(category.id);
|
||||||
|
await fetchCategories();
|
||||||
|
toast.add({severity: 'info', summary: 'Confirmed', detail: 'Record deleted', life: 3000});
|
||||||
|
},
|
||||||
|
reject: () => {
|
||||||
|
toast.add({severity: 'error', summary: 'Rejected', detail: 'You have rejected', life: 3000});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(confirm)
|
||||||
|
};
|
||||||
|
|
||||||
const deleteCat = async (categoryId: number) => {
|
const deleteCat = async (categoryId: number) => {
|
||||||
|
|
||||||
await deleteCategory(categoryId);
|
await deleteCategory(categoryId);
|
||||||
await fetchCategories();
|
await fetchCategories();
|
||||||
}
|
}
|
||||||
@@ -226,26 +256,7 @@ export default {
|
|||||||
loading.value = false;
|
loading.value = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
|
||||||
expenseCategories,
|
|
||||||
incomeCategories,
|
|
||||||
selectedCategoryType,
|
|
||||||
categoryTypes,
|
|
||||||
searchTerm,
|
|
||||||
filteredExpenseCategories,
|
|
||||||
filteredIncomeCategories,
|
|
||||||
filteredCategories,
|
|
||||||
isDialogVisible,
|
|
||||||
openCreateDialog,
|
|
||||||
closeCreateDialog,
|
|
||||||
saveCategory,
|
|
||||||
deleteCat,
|
|
||||||
openEdit,
|
|
||||||
editingCategory,
|
|
||||||
loading,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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"/>
|
||||||
|
|||||||
@@ -14,20 +14,20 @@
|
|||||||
|
|
||||||
<!-- SelectButton для выбора типа категории -->
|
<!-- SelectButton для выбора типа категории -->
|
||||||
<div class="flex justify-center mt-4">
|
<div class="flex justify-center mt-4">
|
||||||
<SelectButton v-model="categoryType" :options="categoryTypes" optionLabel="name" />
|
<SelectButton v-if="!isEditing" v-model="categoryType" :options="categoryTypes" optionLabel="name"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Поля для создания/редактирования категории -->
|
<!-- Поля для создания/редактирования категории -->
|
||||||
<label for="newCategoryName">Category Name:</label>
|
<label for="newCategoryName">Название категории:</label>
|
||||||
<input v-model="name" type="text" id="newCategoryName"/>
|
<input v-model="name" type="text" id="newCategoryName"/>
|
||||||
|
|
||||||
<label for="newCategoryDesc">Category Description:</label>
|
<label for="newCategoryDesc">Описание категории:</label>
|
||||||
<input v-model="description" type="text" id="newCategoryDesc"/>
|
<input v-model="description" type="text" id="newCategoryDesc"/>
|
||||||
|
|
||||||
<!-- Кнопки -->
|
<!-- Кнопки -->
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
<button @click="saveCategory" class="create-category-btn">{{ isEditing ? 'Save' : 'Create' }}</button>
|
<button @click="saveCategory" class="create-category-btn">{{ isEditing ? 'Сохранить' : 'Создать' }}</button>
|
||||||
<button @click="closeModal" class="close-modal-btn">Cancel</button>
|
<button @click="closeModal" class="close-modal-btn">Отмена</button>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -71,7 +71,7 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const emojis = ['😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂', '🙂', '🙃', '😉', '😊', '😇', '😍', '🥰', '😘'];
|
const emojis = ['😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂', '🙂', '🙃', '😉', '😊', '😇', '😍', '🥰', '😘', '🍎', '🍔', '🍕', '🍣', '☕', '🍺', '🚗', '🚕', '🚴♂️', '🚆', '✈️', '⛴️', '🛒', '👗', '💍', '👟', '🛍️', '🎮', '🎥', '🎧', '🎢', '🎨', '🏠', '🛏️', '🧹', '🪴', '🍼', '🏥', '💊', '🩺', '🦷', '💳', '💰', '🏦', '🌍', '🗺️', '🏝️', '🏔️', '💻', '📚', '🖋️', '🏫'];
|
||||||
|
|
||||||
const toggleEmojiPicker = () => {
|
const toggleEmojiPicker = () => {
|
||||||
showEmojiPicker.value = !showEmojiPicker.value;
|
showEmojiPicker.value = !showEmojiPicker.value;
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import DatePicker from "primevue/datepicker";
|
|||||||
import FloatLabel from "primevue/floatlabel";
|
import FloatLabel from "primevue/floatlabel";
|
||||||
import InputNumber from "primevue/inputnumber";
|
import InputNumber from "primevue/inputnumber";
|
||||||
import Button from "primevue/button";
|
import Button from "primevue/button";
|
||||||
import {ref, onMounted, computed} from 'vue';
|
import {ref, onMounted, computed, nextTick} from 'vue';
|
||||||
import {Transaction, TransactionType} from "@/models/Transaction";
|
import {Transaction, TransactionType} from "@/models/Transaction";
|
||||||
import {CategoryType} from "@/models/Category";
|
import {CategoryType} from "@/models/Category";
|
||||||
import SelectButton from "primevue/selectbutton";
|
import SelectButton from "primevue/selectbutton";
|
||||||
@@ -35,10 +35,6 @@ const props = defineProps({
|
|||||||
categoryType: {
|
categoryType: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false
|
required: false
|
||||||
},
|
|
||||||
transactions: {
|
|
||||||
type: Array as () => Array<Transaction>,
|
|
||||||
required: false
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -103,7 +99,7 @@ const checkForm = () => {
|
|||||||
amount: 'Сумма не может быть пустой или 0'
|
amount: 'Сумма не может быть пустой или 0'
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!editedTransaction.value.transactionType) return showError(errorMessages.transactionType);
|
if (!editedTransaction.value.type) return showError(errorMessages.transactionType);
|
||||||
if (!editedTransaction.value.category) return showError(errorMessages.category);
|
if (!editedTransaction.value.category) return showError(errorMessages.category);
|
||||||
if (!editedTransaction.value.date) return showError(errorMessages.date);
|
if (!editedTransaction.value.date) return showError(errorMessages.date);
|
||||||
if (!editedTransaction.value.comment) return showError(errorMessages.comment);
|
if (!editedTransaction.value.comment) return showError(errorMessages.comment);
|
||||||
@@ -116,7 +112,7 @@ const checkForm = () => {
|
|||||||
const prepareData = () => {
|
const prepareData = () => {
|
||||||
if (!props.transaction) {
|
if (!props.transaction) {
|
||||||
editedTransaction.value = new Transaction();
|
editedTransaction.value = new Transaction();
|
||||||
editedTransaction.value.transactionType = transactionTypes.value.find(type => type.code === props.transactionType) || transactionTypes.value[0];
|
editedTransaction.value.type = transactionTypes.value.find(type => type.code === props.transactionType) || transactionTypes.value[0];
|
||||||
selectedCategoryType.value = categoryTypes.value.find(type => type.code === props.categoryType) || categoryTypes.value[0];
|
selectedCategoryType.value = categoryTypes.value.find(type => type.code === props.categoryType) || categoryTypes.value[0];
|
||||||
editedTransaction.value.category = props.categoryType === 'EXPENSE' ? expenseCategories.value[0] : incomeCategories.value[0];
|
editedTransaction.value.category = props.categoryType === 'EXPENSE' ? expenseCategories.value[0] : incomeCategories.value[0];
|
||||||
editedTransaction.value.date = new Date();
|
editedTransaction.value.date = new Date();
|
||||||
@@ -158,20 +154,29 @@ const showError = (message) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Создание транзакции
|
// Создание транзакции
|
||||||
|
const amountInput = ref(null);
|
||||||
const createTransaction = async () => {
|
const createTransaction = async () => {
|
||||||
if (checkForm()) {
|
if (checkForm()) {
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
if (editedTransaction.value.transactionType.code === 'INSTANT') {
|
if (editedTransaction.value.type.code === 'INSTANT') {
|
||||||
editedTransaction.value.isDone = true;
|
editedTransaction.value.isDone = true;
|
||||||
}
|
}
|
||||||
await createTransactionRequest(editedTransaction.value);
|
await createTransactionRequest(editedTransaction.value);
|
||||||
toast.add({severity: 'success', summary: 'Транзакция создана!', detail: 'Транзакция создана!', life: 3000});
|
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
amountInput.value.$el.querySelector('input').focus()
|
||||||
|
}, 10)
|
||||||
|
|
||||||
|
|
||||||
emit('create-transaction', editedTransaction.value);
|
emit('create-transaction', editedTransaction.value);
|
||||||
computeResult(true)
|
toast.add({severity: 'success', summary: 'Успешно!', detail: 'Транзакция создана!', life: 3000});
|
||||||
|
// computeResult(true)
|
||||||
resetForm();
|
resetForm();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
computeResult(false, error)
|
// computeResult(false, error)
|
||||||
|
toast.add({severity: 'error', summary: 'Ошибка!', detail: error.response.data["message"], life: 3000});
|
||||||
console.error('Error creating transaction:', error);
|
console.error('Error creating transaction:', error);
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
@@ -193,9 +198,9 @@ const updateTransaction = async () => {
|
|||||||
// toast.add({severity: 'success', summary: 'Transaction updated!', detail: 'Транзакция обновлена!', life: 3000});
|
// toast.add({severity: 'success', summary: 'Transaction updated!', detail: 'Транзакция обновлена!', life: 3000});
|
||||||
emit('update-transaction', editedTransaction.value);
|
emit('update-transaction', editedTransaction.value);
|
||||||
emit('transaction-updated');
|
emit('transaction-updated');
|
||||||
computeResult(true)
|
toast.add({severity: 'success', summary: 'Успешно!', detail: 'Транзакция создана!', life: 3000});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
computeResult(false, error)
|
toast.add({severity: 'error', summary: 'Ошибка!', detail: error.response.data["message"], life: 3000});
|
||||||
console.error('Error updating transaction:', error);
|
console.error('Error updating transaction:', error);
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
@@ -216,10 +221,10 @@ const deleteTransaction = async () => {
|
|||||||
toast.add({severity: 'success', summary: 'Транзакция удалена!', detail: 'Транзакция удалена!', life: 3000});
|
toast.add({severity: 'success', summary: 'Транзакция удалена!', detail: 'Транзакция удалена!', life: 3000});
|
||||||
emit('delete-transaction', editedTransaction.value);
|
emit('delete-transaction', editedTransaction.value);
|
||||||
closeDrawer()
|
closeDrawer()
|
||||||
computeResult(true)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
computeResult(false, error)
|
|
||||||
toast.add({severity: 'warn', summary: 'Error!', detail: 'Транзакция обновлена!', life: 3000});
|
toast.add({severity: 'error', summary: 'Ошибка!', detail: error.response.data["message"], life: 3000});
|
||||||
|
|
||||||
console.error('Error deleting transaction:', error);
|
console.error('Error deleting transaction:', error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -238,10 +243,10 @@ const resetForm = () => {
|
|||||||
|
|
||||||
const dateErrorMessage = computed(() => {
|
const dateErrorMessage = computed(() => {
|
||||||
|
|
||||||
if (editedTransaction.value.transactionType.code != 'PLANNED' && editedTransaction.value.date > new Date()) {
|
if (editedTransaction.value.type.code != 'PLANNED' && editedTransaction.value.date > new Date()) {
|
||||||
|
|
||||||
return 'При мгновенных тратах дата должна быть меньше текущей!'
|
return 'При мгновенных тратах дата должна быть меньше текущей!'
|
||||||
} else if (editedTransaction.value.transactionType.code == 'PLANNED' && editedTransaction.value.date < new Date()) {
|
} else if (editedTransaction.value.type.code == 'PLANNED' && editedTransaction.value.date < new Date()) {
|
||||||
|
|
||||||
return 'При плановых тратах дата должна быть больше текущей!'
|
return 'При плановых тратах дата должна быть больше текущей!'
|
||||||
} else {
|
} else {
|
||||||
@@ -255,7 +260,7 @@ const closeDrawer = () => emit('close-drawer');
|
|||||||
const keyboardOpen = ref(false);
|
const keyboardOpen = ref(false);
|
||||||
const isMobile = ref(false);
|
const isMobile = ref(false);
|
||||||
const userAgent = ref(null);
|
const userAgent = ref(null);
|
||||||
const transactions = ref<Transaction[]>(props.transactions);
|
const transactions = ref<Transaction[]>(null);
|
||||||
// Мониторинг при монтировании
|
// Мониторинг при монтировании
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|
||||||
@@ -264,16 +269,18 @@ onMounted(async () => {
|
|||||||
await fetchCategoriesAndTypes();
|
await fetchCategoriesAndTypes();
|
||||||
|
|
||||||
prepareData();
|
prepareData();
|
||||||
|
console.log("is editing " + !isEditing.value)
|
||||||
if (!isEditing.value) {
|
if (!isEditing.value) {
|
||||||
await getTransactions('INSTANT', 'EXPENSE',null, user.value.id ).then(transactionsResponse => transactions.value = transactionsResponse.data);
|
console.log("is editing " + !isEditing.value)
|
||||||
transactions.value = transactions.value.slice(0,3)
|
await getTransactions('INSTANT', 'EXPENSE', null, user.value.id, false, 3).then(transactionsResponse => transactions.value = transactionsResponse.data);
|
||||||
|
// transactions.value = transactions.value.slice(0,3)
|
||||||
console.log(transactions.value.slice(0, 3))
|
console.log(transactions.value.slice(0, 3))
|
||||||
}
|
}
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
const deviceInfo = platform;
|
const deviceInfo = platform;
|
||||||
isMobile.value = deviceInfo.os.family === 'iOS' || deviceInfo.os.family === 'Android';
|
isMobile.value = deviceInfo.os.family === 'iOS' || deviceInfo.os.family === 'Android';
|
||||||
|
await nextTick();
|
||||||
|
console.log('Amount Input Ref:', amountInput.value);
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -303,7 +310,7 @@ onMounted(async () => {
|
|||||||
<div class="relative w-full justify-center justify-items-center ">
|
<div class="relative w-full justify-center justify-items-center ">
|
||||||
<div class="flex flex-col justify-items-center gap-2">
|
<div class="flex flex-col justify-items-center gap-2">
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<Select v-if="!isEditing" v-model="editedTransaction.transactionType" :allow-empty="false"
|
<Select v-if="!isEditing" v-model="editedTransaction.type" :allow-empty="false"
|
||||||
:options="transactionTypes"
|
:options="transactionTypes"
|
||||||
optionLabel="name"
|
optionLabel="name"
|
||||||
aria-labelledby="basic"
|
aria-labelledby="basic"
|
||||||
@@ -360,6 +367,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<FloatLabel variant="on" class="">
|
<FloatLabel variant="on" class="">
|
||||||
<InputNumber class=""
|
<InputNumber class=""
|
||||||
|
ref="amountInput"
|
||||||
:invalid="!editedTransaction.amount"
|
:invalid="!editedTransaction.amount"
|
||||||
:minFractionDigits="0"
|
:minFractionDigits="0"
|
||||||
id="amount"
|
id="amount"
|
||||||
@@ -396,7 +404,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<DatePicker class="w-full"
|
<DatePicker class="w-full"
|
||||||
inline
|
inline
|
||||||
:invalid="editedTransaction.transactionType.code != 'PLANNED' ? editedTransaction.date > new Date() : true"
|
:invalid="editedTransaction.type.code != 'PLANNED' ? editedTransaction.date > new Date() : true"
|
||||||
id="date"
|
id="date"
|
||||||
v-model="editedTransaction.date"
|
v-model="editedTransaction.date"
|
||||||
dateFormat="yy-mm-dd"
|
dateFormat="yy-mm-dd"
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ import BudgetTransactionView from "@/components/budgets/BudgetTransactionView.vu
|
|||||||
import IconField from "primevue/iconfield";
|
import IconField from "primevue/iconfield";
|
||||||
import InputIcon from "primevue/inputicon";
|
import InputIcon from "primevue/inputicon";
|
||||||
import InputText from "primevue/inputtext";
|
import InputText from "primevue/inputtext";
|
||||||
|
|
||||||
import {getTransactions} from "@/services/transactionService";
|
import {getTransactions} from "@/services/transactionService";
|
||||||
import {Transaction} from "@/models/Transaction";
|
import {Transaction} from "@/models/Transaction";
|
||||||
import ProgressSpinner from "primevue/progressspinner";
|
import ProgressSpinner from "primevue/progressspinner";
|
||||||
|
import {getUsers} from "@/services/userService";
|
||||||
|
import Button from "primevue/button";
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const searchText = ref("");
|
const searchText = ref("");
|
||||||
@@ -16,12 +19,15 @@ const offset = ref(0); // Начальное смещение
|
|||||||
const allLoaded = ref(false); // Флаг для отслеживания окончания данных
|
const allLoaded = ref(false); // Флаг для отслеживания окончания данных
|
||||||
|
|
||||||
// Функция для получения транзакций с параметрами limit и offset
|
// Функция для получения транзакций с параметрами limit и offset
|
||||||
const fetchTransactions = async () => {
|
const fetchTransactions = async (reload) => {
|
||||||
if (loading.value || allLoaded.value) return; // Останавливаем загрузку, если уже загружается или данные загружены полностью
|
console.log("here")
|
||||||
|
console.log(allLoaded.value)
|
||||||
|
// if (loading.value || allLoaded.value) return; // Останавливаем загрузку, если уже загружается или данные загружены полностью
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await getTransactions('INSTANT', null,null, null, limit, offset.value);
|
console.log(reload);
|
||||||
|
const response = await getTransactions('INSTANT', null, null, selectedUserId.value ? selectedUserId.value : null, null, reload ? offset.value : limit, reload ? 0 : offset.value);
|
||||||
const newTransactions = response.data;
|
const newTransactions = response.data;
|
||||||
|
|
||||||
// Проверка на конец данных
|
// Проверка на конец данных
|
||||||
@@ -30,7 +36,8 @@ const fetchTransactions = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Добавляем новые транзакции к текущему списку
|
// Добавляем новые транзакции к текущему списку
|
||||||
transactions.value.push(...newTransactions);
|
|
||||||
|
reload ? transactions.value = newTransactions : transactions.value.push(...newTransactions)
|
||||||
offset.value += limit; // Обновляем смещение для следующей загрузки
|
offset.value += limit; // Обновляем смещение для следующей загрузки
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching transactions:", error);
|
console.error("Error fetching transactions:", error);
|
||||||
@@ -38,6 +45,19 @@ const fetchTransactions = async () => {
|
|||||||
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
};
|
};
|
||||||
|
const switchUserFilter = async (user) => {
|
||||||
|
if (selectedUserId.value == user.id) {
|
||||||
|
selectedUserId.value = null
|
||||||
|
} else if (selectedUserId.value == null) {
|
||||||
|
selectedUserId.value = user.id;
|
||||||
|
} else {
|
||||||
|
selectedUserId.value = user.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
await getTransactions('INSTANT', null, null, selectedUserId.value, null, offset.value, 0)
|
||||||
|
.then(it => transactions.value = it.data)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
const tgname = computed(() => {
|
const tgname = computed(() => {
|
||||||
if (window.Telegram.WebApp) {
|
if (window.Telegram.WebApp) {
|
||||||
@@ -48,30 +68,45 @@ const tgname = computed(() => {
|
|||||||
|
|
||||||
// Отфильтрованные транзакции по поисковому запросу
|
// Отфильтрованные транзакции по поисковому запросу
|
||||||
const filteredTransactions = computed(() => {
|
const filteredTransactions = computed(() => {
|
||||||
if (searchText.value.length === 0) {
|
// Проверяем, есть ли текст поиска
|
||||||
|
const search = searchText.value.trim().toLowerCase();
|
||||||
|
|
||||||
|
if (!search) {
|
||||||
|
// Если текст поиска пуст, возвращаем все транзакции
|
||||||
return transactions.value;
|
return transactions.value;
|
||||||
} else {
|
|
||||||
return transactions.value.filter(transaction => {
|
|
||||||
const search = searchText.value.toLowerCase();
|
|
||||||
return (
|
|
||||||
transaction.transaction.comment.toLowerCase().includes(search) ||
|
|
||||||
transaction.transaction.category.name.toLowerCase().includes(search)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Проверяем наличие данных
|
||||||
|
if (!transactions.value || !Array.isArray(transactions.value)) {
|
||||||
|
console.warn("Transactions is not a valid array");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Фильтруем транзакции по тексту поиска
|
||||||
|
return transactions.value.filter(transaction => {
|
||||||
|
return transaction.comment.toLowerCase().includes(search) ||
|
||||||
|
transaction.category.name.toLowerCase().includes(search);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// Обработчик прокрутки для ленивой загрузки
|
// Обработчик прокрутки для ленивой загрузки
|
||||||
const handleScroll = () => {
|
// const handleScroll = () => {
|
||||||
const bottomReached = window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 2000;
|
// const bottomReached = window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 2000;
|
||||||
if (bottomReached && !loading.value) {
|
// if (bottomReached && !loading.value) {
|
||||||
fetchTransactions(); // Загружаем следующую страницу
|
// fetchTransactions(); // Загружаем следующую страницу
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
|
const users = ref([])
|
||||||
|
const selectedUserId = ref(null)
|
||||||
|
const fetchUsers = async () => {
|
||||||
|
users.value = await getUsers();
|
||||||
|
}
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await fetchTransactions(); // Первоначальная загрузка данных
|
await fetchTransactions(); // Первоначальная загрузка данных
|
||||||
window.addEventListener("scroll", handleScroll); // Добавляем обработчик прокрутки
|
await fetchUsers();
|
||||||
|
// window.addEventListener("scroll", handleScroll); // Добавляем обработчик прокрутки
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -84,15 +119,27 @@ onMounted(async () => {
|
|||||||
<InputIcon class="pi pi-search"/>
|
<InputIcon class="pi pi-search"/>
|
||||||
<InputText v-model="searchText" placeholder="Search"></InputText>
|
<InputText v-model="searchText" placeholder="Search"></InputText>
|
||||||
</IconField>
|
</IconField>
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<!-- <span v-for="user in users">{{user.id}}</span>-->
|
||||||
|
<button v-for="user in users" @click="switchUserFilter(user)"
|
||||||
|
class="rounded-xl border p-1 bg-white border-gray-300 mb-2 min-w-fit px-2"
|
||||||
|
:class="selectedUserId == user.id ? '!bg-blue-100' : ''">
|
||||||
|
<p><span class="text-sm font-bold">{{ user.firstName }}</span></p>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
|
|
||||||
<BudgetTransactionView
|
<BudgetTransactionView
|
||||||
v-for="transaction in filteredTransactions"
|
v-for="transaction in filteredTransactions"
|
||||||
:key="transaction.id"
|
:key="transaction.id"
|
||||||
:transaction="transaction"
|
:transaction="transaction"
|
||||||
:is-list="true"
|
:is-list="true"
|
||||||
@transaction-updated="fetchTransactions"
|
@transaction-updated="fetchTransactions(true)"
|
||||||
|
@delete-transaction="fetchTransactions(true)"
|
||||||
/>
|
/>
|
||||||
|
<div class="flex items-center justify-center px-2 py-1 mb-5">
|
||||||
|
<Button @click="fetchTransactions(false)">Загрузить следующие...</Button>
|
||||||
|
</div>
|
||||||
<!-- Показать спиннер загрузки, если идет загрузка -->
|
<!-- Показать спиннер загрузки, если идет загрузка -->
|
||||||
<ProgressSpinner v-if="loading" class="mb-4" style="width: 50px; height: 50px;"
|
<ProgressSpinner v-if="loading" class="mb-4" style="width: 50px; height: 50px;"
|
||||||
strokeWidth="8"
|
strokeWidth="8"
|
||||||
|
|||||||
@@ -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: ["Воскресенье", "Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота"],
|
||||||
|
|||||||
@@ -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 // отображает текущую сумму запланированных расходов по категории
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import {Category} from "@/models/Category";
|
import {Category} from "@/models/Category";
|
||||||
|
import {User} from "@/models/User";
|
||||||
|
|
||||||
export class Transaction {
|
export class Transaction {
|
||||||
id: number;
|
id: String;
|
||||||
transactionType: TransactionType;
|
type: TransactionType;
|
||||||
|
user: User
|
||||||
category: Category;
|
category: Category;
|
||||||
comment: string;
|
comment: string;
|
||||||
date: Date;
|
date: Date;
|
||||||
amount: number
|
amount: number
|
||||||
|
parentId: String
|
||||||
isDone: boolean;
|
isDone: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,8 +19,6 @@ export class TransactionCategoriesSum{
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export class TransactionType {
|
export class TransactionType {
|
||||||
code: string;
|
code: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -4,25 +4,32 @@ import {format} from "date-fns";
|
|||||||
// Импортируете настроенный экземпляр axios
|
// Импортируете настроенный экземпляр axios
|
||||||
|
|
||||||
export const getBudgetInfos = async () => {
|
export const getBudgetInfos = async () => {
|
||||||
|
try {
|
||||||
|
|
||||||
let response = await apiClient.get('/budgets/');
|
|
||||||
|
let response = await apiClient.get('/budgets');
|
||||||
let budgetInfos = response.data;
|
let budgetInfos = response.data;
|
||||||
budgetInfos.forEach((budgetInfo: Budget) => {
|
budgetInfos.forEach((budgetInfo: Budget) => {
|
||||||
budgetInfo.dateFrom = new Date(budgetInfo.dateFrom);
|
budgetInfo.dateFrom = new Date(budgetInfo.dateFrom);
|
||||||
budgetInfo.dateTo = new Date(budgetInfo.dateTo);
|
budgetInfo.dateTo = new Date(budgetInfo.dateTo);
|
||||||
// budgetInfo.plannedExpenses.forEach(e => {
|
budgetInfo.plannedExpenses?.forEach(e => {
|
||||||
// e.date = new Date(e.date)
|
e.date = new Date(e.date)
|
||||||
// })
|
})
|
||||||
//
|
|
||||||
// budgetInfo.plannedIncomes.forEach(e => {
|
budgetInfo.plannedIncomes?.forEach(e => {
|
||||||
// e.date = new Date(e.date)
|
e.date = new Date(e.date)
|
||||||
// })
|
})
|
||||||
//
|
|
||||||
// budgetInfo.transactions.forEach(e => {
|
budgetInfo.transactions?.forEach(e => {
|
||||||
// e.date = new Date(e.date)
|
e.date = new Date(e.date)
|
||||||
// })
|
})
|
||||||
})
|
})
|
||||||
return budgetInfos
|
return budgetInfos
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
throw e
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getBudgetTransactions = async (budgetId, transactionType, categoryType) => {
|
export const getBudgetTransactions = async (budgetId, transactionType, categoryType) => {
|
||||||
@@ -66,18 +73,50 @@ export const getBudgetInfo = async (budget_id: number) => {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const getWarns = async (budgetId: string, hidden: Boolean = null) => {
|
||||||
|
let url = `/budgets/${budgetId}/warns`
|
||||||
|
if (hidden) {
|
||||||
|
url += `?hidden=${hidden}`
|
||||||
|
}
|
||||||
|
let warns = await apiClient.get(url);
|
||||||
|
return warns.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hideWarnRequest = async (budgetId: string, warnId: string) => {
|
||||||
|
await apiClient.post(`/budgets/${budgetId}/warns/${warnId}/hide`);
|
||||||
|
}
|
||||||
|
|
||||||
export const updateBudgetCategoryRequest = async (budget_id, category: BudgetCategory) => {
|
export const updateBudgetCategoryRequest = async (budget_id, category: BudgetCategory) => {
|
||||||
await apiClient.put('/budgets/' + budget_id + '/category', category);
|
return await apiClient.post('/budgets/' + budget_id + '/categories/' + category.category.id + "/limit", {"limit": category.currentLimit}).then(i => i.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createBudget = async (budget: Budget, createRecurrent: Boolean) => {
|
export const createBudget = async (budget: Budget, createRecurrent: Boolean) => {
|
||||||
budget.dateFrom = format(budget.dateFrom, 'yyyy-MM-dd')
|
|
||||||
budget.dateTo = format(budget.dateTo, 'yyyy-MM-dd')
|
try {
|
||||||
|
|
||||||
|
let budgetToCreate = JSON.parse(JSON.stringify(budget));
|
||||||
|
budgetToCreate.dateFrom = format(budget.dateFrom, 'yyyy-MM-dd')
|
||||||
|
budgetToCreate.dateTo = format(budget.dateTo, 'yyyy-MM-dd')
|
||||||
let data = {
|
let data = {
|
||||||
budget: budget,
|
budget: budgetToCreate,
|
||||||
createRecurrent: createRecurrent
|
createRecurrent: createRecurrent
|
||||||
}
|
}
|
||||||
await apiClient.post('/budgets', data);
|
await apiClient.post('/budgets/', data);
|
||||||
budget.dateFrom = format(budget.dateFrom, 'dd.mm.yy')
|
|
||||||
budget.dateTo = format(budget.dateTo, 'dd.mm.yy')
|
} catch (e){
|
||||||
|
console.error(e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const deleteBudgetRequest = async (budgetId: string) => {
|
||||||
|
try {
|
||||||
|
// throw Error("test")
|
||||||
|
let response = await apiClient.delete(`/budgets/${budgetId}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ export const getTransaction = async (transactionId: int) => {
|
|||||||
return await apiClient.post(`/transactions/${transactionId}`,);
|
return await apiClient.post(`/transactions/${transactionId}`,);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTransactions = async (transaction_type = null, category_type = null, category_id = null, user_id = null, limit = null, offset = null) => {
|
export const getTransactions = async (transaction_type = null, category_type = null, category_id = null, user_id = null, is_child = null, limit = null, offset = null) => {
|
||||||
const params = {};
|
const params = {};
|
||||||
|
|
||||||
|
console.log(is_child)
|
||||||
// Add the parameters to the params object if they are not null
|
// Add the parameters to the params object if they are not null
|
||||||
if (transaction_type) {
|
if (transaction_type) {
|
||||||
params.transaction_type = transaction_type;
|
params.transaction_type = transaction_type;
|
||||||
@@ -25,6 +26,9 @@ export const getTransactions = async (transaction_type = null, category_type = n
|
|||||||
if (user_id) {
|
if (user_id) {
|
||||||
params.user_id = user_id
|
params.user_id = user_id
|
||||||
}
|
}
|
||||||
|
if (is_child!=null){
|
||||||
|
params.is_child = is_child
|
||||||
|
}
|
||||||
if (limit) {
|
if (limit) {
|
||||||
params.limit = limit
|
params.limit = limit
|
||||||
}
|
}
|
||||||
@@ -33,7 +37,7 @@ export const getTransactions = async (transaction_type = null, category_type = n
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use axios to make the GET request, passing the params as the second argument
|
// Use axios to make the GET request, passing the params as the second argument
|
||||||
return await apiClient.get('/transactions/', {
|
return await apiClient.get('/transactions', {
|
||||||
params: params
|
params: params
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -41,14 +45,17 @@ export const getTransactions = async (transaction_type = null, category_type = n
|
|||||||
export const createTransactionRequest = async (transaction: Transaction) => {
|
export const createTransactionRequest = async (transaction: Transaction) => {
|
||||||
transaction.date = format(transaction.date, 'yyyy-MM-dd')
|
transaction.date = format(transaction.date, 'yyyy-MM-dd')
|
||||||
let transactionResponse = await apiClient.post('/transactions', transaction);
|
let transactionResponse = await apiClient.post('/transactions', transaction);
|
||||||
console.log(transaction.date)
|
|
||||||
transaction.date = new Date(transaction.date);
|
transaction.date = new Date(transaction.date);
|
||||||
return transactionResponse.data
|
return transactionResponse.data
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateTransactionRequest = async (transaction: Transaction) => {
|
export const updateTransactionRequest = async (transaction: Transaction) => {
|
||||||
const id = transaction.id
|
const id = transaction.id
|
||||||
transaction.date = format(transaction.date, 'yyyy-MM-dd')
|
// console.log(transaction.isDone)
|
||||||
|
// transaction.date = transaction.date.setHours(0,0,0,0)
|
||||||
|
transaction.date = format(transaction.date, "yyyy-MM-dd")
|
||||||
|
|
||||||
const response = await apiClient.put(`/transactions/${id}`, transaction);
|
const response = await apiClient.put(`/transactions/${id}`, transaction);
|
||||||
transaction = response.data
|
transaction = response.data
|
||||||
transaction.date = new Date(transaction.date);
|
transaction.date = new Date(transaction.date);
|
||||||
@@ -56,15 +63,15 @@ export const updateTransactionRequest = async (transaction: Transaction) => {
|
|||||||
return transaction
|
return transaction
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setTransactionDoneRequest = async (transaction: Transaction) => {
|
// export const setTransactionDoneRequest = async (transaction: Transaction) => {
|
||||||
const id = transaction.id
|
// const id = transaction.id
|
||||||
// transaction.date = format(transaction.date, 'yyyy-MM-dd')
|
// // transaction.date = format(transaction.date, 'yyyy-MM-dd')
|
||||||
const response = await apiClient.put(`/transactions/${id}/done`, transaction);
|
// const response = await apiClient.patch(`/transactions/${id}/set-done`, transaction);
|
||||||
transaction = response.data
|
// // transaction = response.data
|
||||||
transaction.date = new Date(transaction.date);
|
// // transaction.date = new Date(transaction.date);
|
||||||
|
//
|
||||||
return transaction
|
// // return transaction
|
||||||
};
|
// };
|
||||||
|
|
||||||
export const deleteTransactionRequest = async (id: number) => {
|
export const deleteTransactionRequest = async (id: number) => {
|
||||||
return await apiClient.delete(`/transactions/${id}`);
|
return await apiClient.delete(`/transactions/${id}`);
|
||||||
|
|||||||
7
src/services/userService.ts
Normal file
7
src/services/userService.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import apiClient from '@/services/axiosSetup';
|
||||||
|
|
||||||
|
|
||||||
|
export const getUsers = async () => {
|
||||||
|
let users = await apiClient.get('/users/');
|
||||||
|
return users.data;
|
||||||
|
}
|
||||||
@@ -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};
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user