ver 2
This commit is contained in:
@@ -2,13 +2,14 @@
|
||||
|
||||
<div id="app" class="flex flex-col h-screen bg-gray-300">
|
||||
<!-- MenuBar всегда фиксирован сверху -->
|
||||
<MenuBar class="w-full sticky hidden lg:block top-0 z-10"/>
|
||||
<MenuBar v-if="userStore.user" class="w-full sticky hidden lg:block top-0 z-10"/>
|
||||
<ToolBar class=" fixed visible lg:invisible bottom-0 z-10"/>
|
||||
|
||||
<!-- Контентная часть заполняет оставшееся пространство -->
|
||||
<div class="flex flex-col flex-grow">
|
||||
<!-- {{ tg_id }}-->
|
||||
<Button label="Sub" :class="checkNotif ? 'flex' : '!hidden'" @click="checkSubscribe"/>
|
||||
|
||||
<router-view />
|
||||
<div class="bg-gray-100 h-12 block lg:hidden"></div>
|
||||
</div>
|
||||
@@ -32,6 +33,7 @@ import {useDrawerStore} from '@/stores/drawerStore'
|
||||
import TransactionForm from "@/components/transactions/TransactionForm.vue";
|
||||
|
||||
|
||||
|
||||
const drawerStore = useDrawerStore();
|
||||
const visible = computed(() => drawerStore.visible);
|
||||
const closeDrawer = () => {
|
||||
@@ -73,7 +75,7 @@ const sendSubscribe = async () => {
|
||||
console.log("Push subscription:", subscription);
|
||||
|
||||
// Отправка подписки на сервер для хранения
|
||||
await apiClient.post("/push/subscribe", subscription)
|
||||
await apiClient.post("/subscriptions/subscribe", subscription)
|
||||
} catch (error) {
|
||||
console.error("Failed to subscribe to push:", error);
|
||||
}
|
||||
|
||||
@@ -17,12 +17,43 @@ ChartJS.register(ChartDataLabels);
|
||||
|
||||
const loading = ref(false);
|
||||
const categoriesSums = ref([])
|
||||
const dataTableCategories = ref([])
|
||||
|
||||
|
||||
const fetchCategoriesSums = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
categoriesSums.value = await getTransactionCategoriesSums()
|
||||
// console.log(categoriesSums.value)
|
||||
for (let category of categoriesSums.value) {
|
||||
// console.log(category)
|
||||
// [{
|
||||
// "category": category[0],
|
||||
// "category"+[category[1]]: category[2]
|
||||
// }]
|
||||
// console.log('test')
|
||||
// console.log('dataTableCategories '+ dataTableCategories.value)
|
||||
let categoryInList = dataTableCategories.value.find((listCategory: Category) => {
|
||||
// console.log(listCategory['category'].id)
|
||||
// console.log(category[0].id)
|
||||
return listCategory['category'].id === category[0].id
|
||||
})
|
||||
console.log('cat in list ' + categoryInList)
|
||||
if (categoryInList) {
|
||||
console.log('cat[1] '+ category[1])
|
||||
console.log('cat[2] '+ category[2])
|
||||
categoryInList[category[1]] = category[2]
|
||||
// console.log(categoryInList)
|
||||
} else {
|
||||
dataTableCategories.value.push({'category': category[0]})
|
||||
dataTableCategories.value.filter((listCategory: Category) => {
|
||||
return listCategory['category'].id === category.id
|
||||
})[category[1]] = category[2]
|
||||
}
|
||||
// console.log(categoryInList)
|
||||
}
|
||||
// console.log(dataTableCategories.value)
|
||||
// console.log(categoriesSums.value)
|
||||
} catch (error) {
|
||||
console.error('Error fetching categories sums:', error);
|
||||
}
|
||||
@@ -37,8 +68,8 @@ const fetchCategories = async () => {
|
||||
try {
|
||||
const response = await getCategories('EXPENSE');
|
||||
categories.value = response.data
|
||||
console.log(categories.value.filter(i => i.id==30))
|
||||
selectedCategories.value.push(categories.value.filter(i => i.id==30)[0])
|
||||
console.log(categories.value.filter(i => i.id == 30))
|
||||
selectedCategories.value.push(categories.value.filter(i => i.id == 30)[0])
|
||||
} catch (error) {
|
||||
console.error('Error fetching categories:', error);
|
||||
}
|
||||
@@ -170,9 +201,9 @@ const setChartOptions = () => {
|
||||
offset: 8, // Отступ от точки
|
||||
labels: {
|
||||
|
||||
font: {
|
||||
weight: 'bold'
|
||||
}
|
||||
font: {
|
||||
weight: 'bold'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -213,8 +244,13 @@ onMounted(async () => {
|
||||
placeholder="Выберите категории"
|
||||
:maxSelectedLabels="3" class="w-full md:w-80"/>
|
||||
|
||||
<Chart v-if="selectedCategories.length > 0" type="line" :data="chartData" :options="chartOptions" class="h-[30rem]"/>
|
||||
<Chart v-if="selectedCategories.length > 0" type="line" :data="chartData" :options="chartOptions"
|
||||
class="h-[30rem]"/>
|
||||
|
||||
{{dataTableCategories}}
|
||||
<DataTable :value="dataTableCategories">
|
||||
<!-- <Column v-for="dataTableCategories"/>-->
|
||||
</DataTable>
|
||||
<!-- {{categories}}-->
|
||||
<!-- {{// chartData}}-->
|
||||
</div>
|
||||
|
||||
@@ -36,10 +36,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import qs from 'qs';
|
||||
import { computed, ref, onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import apiClient from '@/services/axiosSetup';
|
||||
import {useUserStore} from "@/stores/userStore";
|
||||
|
||||
const username = ref('');
|
||||
const password = ref('');
|
||||
@@ -64,7 +64,7 @@ const autoLoginWithTgId = async () => {
|
||||
const token = response.data.access_token;
|
||||
localStorage.setItem('token', token);
|
||||
apiClient.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
||||
await router.push(route.query['back'] ? route.query['back'].toString() : '/');
|
||||
await router.replace(route.query['back']+"/reload" ? route.query['back'].toString() : '/');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert('Ошибка входа. Проверьте логин и пароль.');
|
||||
@@ -77,27 +77,29 @@ onMounted(() => {
|
||||
autoLoginWithTgId();
|
||||
});
|
||||
|
||||
const userStore = useUserStore();
|
||||
// Основная функция для логина
|
||||
const login = async () => {
|
||||
try {
|
||||
let response;
|
||||
if (tg_id.value) {
|
||||
response = await apiClient.post('/auth/token/tg', qs.stringify({ tg_id: tg_id.value }));
|
||||
} else {
|
||||
response = await apiClient.post('/auth/token', qs.stringify({
|
||||
username: username.value,
|
||||
password: password.value,
|
||||
}));
|
||||
}
|
||||
|
||||
const token = response.data.access_token;
|
||||
localStorage.setItem('token', token);
|
||||
apiClient.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
||||
await router.push(route.query['back'] ? route.query['back'].toString() : '/');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert('Ошибка входа. Проверьте логин и пароль.');
|
||||
}
|
||||
await userStore.login(username.value, password.value);
|
||||
// try {
|
||||
// let response;
|
||||
// if (tg_id.value) {
|
||||
// response = await apiClient.post('/auth/token/tg', qs.stringify({ tg_id: tg_id.value }));
|
||||
// } else {
|
||||
// response = await apiClient.post('/auth/login', {
|
||||
// username: username.value,
|
||||
// password: password.value,
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// const token = response.data.token;
|
||||
// localStorage.setItem('token', token);
|
||||
// apiClient.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
||||
// await router.push(route.query['back'] ? route.query['back'].toString() : '/');
|
||||
// } catch (error) {
|
||||
// console.error(error);
|
||||
// alert('Ошибка входа. Проверьте логин и пароль.');
|
||||
// }
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ const props = defineProps({
|
||||
required: true
|
||||
},
|
||||
budgetId: {
|
||||
type: Number,
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
@@ -36,7 +36,7 @@ const stopEditing = () => {
|
||||
const spentPlannedRatio = computed(() => {
|
||||
return props.category.currentLimit
|
||||
? (props.category.currentSpent / props.category.currentLimit) * 100
|
||||
: 0;
|
||||
: props.category.currentSpent > 0 ? props.category.currentSpent : 0;
|
||||
});
|
||||
|
||||
// Синхронизация `currentLimit` с `props.category.currentLimit` при обновлении
|
||||
|
||||
@@ -8,7 +8,6 @@ import DatePicker from "primevue/datepicker";
|
||||
import {onMounted, ref} from "vue";
|
||||
import {getMonthName} from "@/utils/utils";
|
||||
import {Budget} from "@/models/Budget";
|
||||
import {createBudget} from "@/services/budgetsService";
|
||||
|
||||
const props = defineProps({
|
||||
opened: {
|
||||
@@ -28,10 +27,10 @@ const budget = ref(new Budget())
|
||||
const create = async () => {
|
||||
console.log(budget.value)
|
||||
try {
|
||||
await createBudget(budget.value, createRecurrentPayments.value)
|
||||
emits("budget-created");
|
||||
emits("budget-created", budget.value, createRecurrentPayments.value);
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
throw e
|
||||
}
|
||||
|
||||
}
|
||||
@@ -40,7 +39,7 @@ const cancel = () => {
|
||||
emits("close-modal");
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const resetForm = () => {
|
||||
budget.value.name = ''
|
||||
budget.value.dateTo = new Date();
|
||||
budget.value.dateFrom = new Date();
|
||||
@@ -58,8 +57,10 @@ onMounted(() => {
|
||||
budget.value.dateTo.setMonth(budget.value.dateTo.getMonth() + 2)
|
||||
}
|
||||
budget.value.name = getMonthName(budget.value.dateFrom.getMonth()) + ' ' + budget.value.dateFrom.getFullYear();
|
||||
}
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
resetForm()
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
@@ -5,81 +5,106 @@
|
||||
<div class="flex flex-row gap-4 items-center">
|
||||
<h2 class="text-4xl font-bold">Бюджеты</h2>
|
||||
<Button label="+ Создать" @click="creationOpened=true" size="small"/>
|
||||
<BudgetCreationView :opened="creationOpened" @budget-created="creationSuccessShow()" @close-modal="creationOpened=false" />
|
||||
<BudgetCreationView :opened="creationOpened" @budget-created="creationSuccessShow"
|
||||
@close-modal="creationOpened=false"/>
|
||||
<StatusView :show="creationSuccessModal" :is-error="false" :message="'Бюджет создан!'"/>
|
||||
</div>
|
||||
<!-- Плитка с бюджетами -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<!-- Будущие и текущие бюджеты -->
|
||||
<ConfirmDialog/>
|
||||
<Toast/>
|
||||
<div v-for="budget in budgetInfos" :key="budget.id" class="p-4 shadow-lg rounded-lg bg-white"
|
||||
:class="budget.dateTo < new Date() ? 'bg-gray-100 opacity-60' : ''">
|
||||
<div class="flex flex-row justify-between">
|
||||
<div class="text-xl font-bold mb-2">{{ budget.name }}</div>
|
||||
<router-link :to="'/budgets/'+budget.id">
|
||||
<i class="pi pi-arrow-circle-right text-green-500" style="font-size: 1.5rem;"/>
|
||||
</router-link>
|
||||
<div class="flex flex-row justify-between gap-4">
|
||||
<div class="flex flex-col justify-between gap-5">
|
||||
<div class="text-xl font-bold ">{{ budget.name }}</div>
|
||||
<div class="text-sm text-gray-600 ">
|
||||
{{ formatDate(budget.dateFrom) }} - {{ formatDate(budget.dateTo) }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="flex flex-col justify-between gap-4">
|
||||
<router-link :to="'/budgets/'+budget.id">
|
||||
<i class="pi pi-arrow-circle-right text-green-500" style="font-size: 1.5rem;"/>
|
||||
</router-link>
|
||||
<button @click="deleteBudget(budget)"><i class="pi pi-trash" style="color:red; font-size: 1.2rem"/></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-gray-600 mb-4">
|
||||
{{ formatDate(budget.dateFrom) }} - {{ formatDate(budget.dateTo) }}
|
||||
</div>
|
||||
<!-- <div class="mb-4">-->
|
||||
<!-- <div class="text-sm">Total Income: <span class="font-bold">{{ formatAmount(budgettotalIncomes) }} ₽</span></div>-->
|
||||
<!-- <div class="text-sm">Total Expenses: <span class="font-bold">{{ formatAmount(budget.totalExpenses) }} ₽</span></div>-->
|
||||
<!-- <div class="text-sm">Planned Expenses: <span class="font-bold">{{ formatAmount(budget.totalExpenses) }} ₽</span></div>-->
|
||||
<!-- <div class="text-sm flex items-center">-->
|
||||
<!-- Unplanned Expenses:-->
|
||||
<!-- <span class="ml-2 font-bold">{{ formatAmount(budget.totalIncomes - budget.totalExpenses) }} ₽</span>-->
|
||||
<!-- Прогресс бар -->
|
||||
<!-- <ProgressBar :value="budget.unplannedProgress" class="ml-4 w-full"/>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<!-- <div class="mb-4">-->
|
||||
<!-- <div class="text-sm">Total Income: <span class="font-bold">{{ formatAmount(budgettotalIncomes) }} ₽</span></div>-->
|
||||
<!-- <div class="text-sm">Total Expenses: <span class="font-bold">{{ formatAmount(budget.totalExpenses) }} ₽</span></div>-->
|
||||
<!-- <div class="text-sm">Planned Expenses: <span class="font-bold">{{ formatAmount(budget.totalExpenses) }} ₽</span></div>-->
|
||||
<!-- <div class="text-sm flex items-center">-->
|
||||
<!-- Unplanned Expenses:-->
|
||||
<!-- <span class="ml-2 font-bold">{{ formatAmount(budget.totalIncomes - budget.totalExpenses) }} ₽</span>-->
|
||||
<!-- Прогресс бар -->
|
||||
<!-- <ProgressBar :value="budget.unplannedProgress" class="ml-4 w-full"/>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
|
||||
<!-- Прошедшие бюджеты (забеленные) -->
|
||||
<!-- <div v-for="budget in pastBudgets" :key="budget.id" class="p-4 shadow-lg rounded-lg bg-gray-100 opacity-60">-->
|
||||
<!-- <div class="text-xl font-bold mb-2">{{ budget.month }}</div>-->
|
||||
<!-- <div class="text-sm text-gray-600 mb-4">-->
|
||||
<!-- {{ budget.startDate }} - {{ budget.endDate }}-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="mb-4">-->
|
||||
<!-- <div class="text-sm">Total Income: <span class="font-bold">{{ budget.totalIncome }}</span></div>-->
|
||||
<!-- <div class="text-sm">Total Expenses: <span class="font-bold">{{ budget.totalExpenses }}</span></div>-->
|
||||
<!-- <div class="text-sm">Planned Expenses: <span class="font-bold">{{ budget.plannedExpenses }}</span></div>-->
|
||||
<!-- <div class="text-sm flex items-center">-->
|
||||
<!-- Unplanned Expenses:-->
|
||||
<!-- <span class="ml-2 font-bold">{{ budget.remainingForUnplanned }}</span>-->
|
||||
<!-- <!– Прогресс бар –>-->
|
||||
<!-- <ProgressBar :value="budget.unplannedProgress" class="ml-4 w-full"/>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div v-for="budget in pastBudgets" :key="budget.id" class="p-4 shadow-lg rounded-lg bg-gray-100 opacity-60">-->
|
||||
<!-- <div class="text-xl font-bold mb-2">{{ budget.month }}</div>-->
|
||||
<!-- <div class="text-sm text-gray-600 mb-4">-->
|
||||
<!-- {{ budget.startDate }} - {{ budget.endDate }}-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="mb-4">-->
|
||||
<!-- <div class="text-sm">Total Income: <span class="font-bold">{{ budget.totalIncome }}</span></div>-->
|
||||
<!-- <div class="text-sm">Total Expenses: <span class="font-bold">{{ budget.totalExpenses }}</span></div>-->
|
||||
<!-- <div class="text-sm">Planned Expenses: <span class="font-bold">{{ budget.plannedExpenses }}</span></div>-->
|
||||
<!-- <div class="text-sm flex items-center">-->
|
||||
<!-- Unplanned Expenses:-->
|
||||
<!-- <span class="ml-2 font-bold">{{ budget.remainingForUnplanned }}</span>-->
|
||||
<!-- <!– Прогресс бар –>-->
|
||||
<!-- <ProgressBar :value="budget.unplannedProgress" class="ml-4 w-full"/>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {onMounted, ref} from 'vue';
|
||||
import {BudgetInfo} from "@/models/Budget";
|
||||
import {getBudgetInfos} from "@/services/budgetsService";
|
||||
import {Budget, BudgetInfo} from "@/models/Budget";
|
||||
import {createBudget, deleteBudgetRequest, getBudgetInfos} from "@/services/budgetsService";
|
||||
import {formatDate} from "@/utils/utils";
|
||||
import LoadingView from "@/components/LoadingView.vue";
|
||||
import Button from "primevue/button";
|
||||
import ConfirmDialog from "primevue/confirmdialog";
|
||||
import BudgetCreationView from "@/components/budgets/BudgetCreationView.vue";
|
||||
import StatusView from "@/components/StatusView.vue";
|
||||
import {useConfirm} from "primevue/useconfirm";
|
||||
import {useToast} from "primevue/usetoast";
|
||||
import Toast from "primevue/toast";
|
||||
|
||||
|
||||
const confirm = useConfirm();
|
||||
const toast = useToast();
|
||||
const loading = ref(false)
|
||||
const budgetInfos = ref<BudgetInfo[]>([])
|
||||
const creationOpened = ref(false)
|
||||
const creationSuccessModal = ref(false)
|
||||
const creationSuccessShow = async () => {
|
||||
creationOpened.value = false
|
||||
budgetInfos.value = await getBudgetInfos()
|
||||
creationSuccessModal.value = true
|
||||
setTimeout(() => {
|
||||
creationSuccessModal.value = false
|
||||
}
|
||||
, 1000)
|
||||
|
||||
const creationSuccessShow = async (budget, createRecurrentPayments) => {
|
||||
|
||||
|
||||
try {
|
||||
await createBudget(budget, createRecurrentPayments)
|
||||
budgetInfos.value = await getBudgetInfos()
|
||||
toast.add({severity: 'success', summary: 'Успешно!', detail: 'Бюджет создан!', life: 3000});
|
||||
creationOpened.value = false
|
||||
} catch (error) {
|
||||
console.log(error.response.data["message"])
|
||||
toast.add({severity: "error", summary: "Бюджет не создан", detail: error.response.data["message"], life: 3000});
|
||||
}
|
||||
// creationSuccessModal.value = true
|
||||
// setTimeout(() => {
|
||||
// creationSuccessModal.value = false
|
||||
// }
|
||||
// , 1000)
|
||||
}
|
||||
const pastBudgets = ref([
|
||||
{
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -6,9 +6,10 @@ import {computed, onMounted, PropType, ref} from "vue";
|
||||
import {Transaction} from "@/models/Transaction";
|
||||
import {Category, CategoryType} from "@/models/Category";
|
||||
import {getCategories, getCategoryTypes} from "@/services/categoryService";
|
||||
import {setTransactionDoneRequest} from "@/services/transactionService";
|
||||
import { updateTransactionRequest} from "@/services/transactionService";
|
||||
import {formatAmount, formatDate} from "@/utils/utils";
|
||||
import TransactionForm from "@/components/transactions/TransactionForm.vue";
|
||||
import {useToast} from "primevue/usetoast";
|
||||
|
||||
|
||||
const props = defineProps(
|
||||
@@ -30,31 +31,44 @@ const props = defineProps(
|
||||
const emits = defineEmits(['open-drawer', 'transaction-checked', 'transaction-updated', 'delete-transaction'])
|
||||
|
||||
|
||||
|
||||
const setIsDoneTrue = async () => {
|
||||
setTimeout(async () => {
|
||||
await setTransactionDoneRequest(props.transaction)
|
||||
emits('transaction-checked')
|
||||
}, 10);
|
||||
console.log("here")
|
||||
await updateTransactionRequest(props.transaction)
|
||||
emits('transaction-updated')
|
||||
}, 20);
|
||||
// showedTransaction.value.isDone = !showedTransaction.value.isDone;
|
||||
|
||||
|
||||
}
|
||||
|
||||
const toast = useToast();
|
||||
const drawerOpened = ref(false)
|
||||
const toggleDrawer = () => {
|
||||
if (drawerOpened.value) {
|
||||
drawerOpened.value = false;
|
||||
if (props.transaction?.parentId) {
|
||||
toast.add({
|
||||
severity: 'warn',
|
||||
summary: 'Транзакцию нельзя изменить!',
|
||||
detail: 'Транзакции созданные из плана не могут быть изменены.',
|
||||
life: 3000
|
||||
});
|
||||
|
||||
} else {
|
||||
|
||||
if (drawerOpened.value) {
|
||||
drawerOpened.value = false;
|
||||
}
|
||||
drawerOpened.value = !drawerOpened.value
|
||||
emits('open-drawer', props.transaction)
|
||||
}
|
||||
drawerOpened.value = !drawerOpened.value
|
||||
emits('open-drawer', props.transaction)
|
||||
}
|
||||
|
||||
const transactionUpdate = () => {
|
||||
console.log("transaction updated")
|
||||
emits('transaction-updated')
|
||||
}
|
||||
|
||||
const isPlanned = computed(() => {
|
||||
return props.transaction?.transactionType.code === "PLANNED"
|
||||
return props.transaction?.type.code === "PLANNED"
|
||||
})
|
||||
|
||||
|
||||
@@ -94,6 +108,8 @@ const closeDrawer = () => {
|
||||
onMounted(async () => {
|
||||
// await fetchCategories();
|
||||
// await fetchCategoryTypes()
|
||||
|
||||
|
||||
})
|
||||
|
||||
</script>
|
||||
@@ -104,12 +120,15 @@ onMounted(async () => {
|
||||
"
|
||||
class="flex bg-white min-w-fit max-h-fit flex-row items-center gap-4 w-full ">
|
||||
<div>
|
||||
<p v-if="transaction.transactionType.code=='INSTANT' || props.isList"
|
||||
<p v-if="transaction.type.code=='INSTANT' || props.isList"
|
||||
class="text-6xl font-bold text-gray-700 dark:text-gray-400">
|
||||
{{ transaction.category.icon }}</p>
|
||||
<Checkbox v-model="transaction.isDone" v-else-if="transaction.transactionType.code=='PLANNED' && !props.isList"
|
||||
{{ transaction.category.icon }}
|
||||
</p>
|
||||
|
||||
<Checkbox v-model="transaction.isDone" v-else-if="transaction.type.code=='PLANNED' && !props.isList"
|
||||
:binary="true"
|
||||
@click="setIsDoneTrue"/>
|
||||
|
||||
</div>
|
||||
<button class="flex flex-row items-center p-x-4 justify-between w-full " @click="toggleDrawer">
|
||||
|
||||
@@ -138,7 +157,7 @@ onMounted(async () => {
|
||||
|
||||
|
||||
<TransactionForm v-if="drawerOpened" :visible="drawerOpened" :transaction="transaction"
|
||||
@close-drawer="closeDrawer" @transaction-updated="transactionUpdate"
|
||||
@close-drawer="closeDrawer" @transaction-updated="transactionUpdate"
|
||||
@delete-transaction="transactionUpdate"
|
||||
@create-transaction="transactionUpdate"/>
|
||||
</div>
|
||||
|
||||
@@ -15,11 +15,44 @@
|
||||
/>
|
||||
</div>
|
||||
<div :class="!updateLoading ? '' : 'h-fit bg-white opacity-50 z-0 '" class=" flex flex-col gap-3">
|
||||
<div class="flex flex-col ">
|
||||
<!-- {{ budget }}-->
|
||||
<h2 class="text-4xl font-bold">Бюджет {{ budget.name }} </h2>
|
||||
<div class="flex flex-row gap-2 text-xl">{{ formatDate(budget.dateFrom) }} -
|
||||
{{ formatDate(budget.dateTo) }}
|
||||
<div class="flex flex-row justify-between ">
|
||||
<div class="flex flex-col gap-2">
|
||||
<h2 class="text-4xl font-bold">Бюджет {{ budget.name }} </h2>
|
||||
<div class="flex flex-row gap-2 text-xl">{{ formatDate(budget.dateFrom) }} -
|
||||
{{ formatDate(budget.dateTo) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2 justify-center">
|
||||
<button class="flex flex-row bg-white py-6 px-4 shadow-lg rounded-lg items-center h-6 justify-center gap-2"
|
||||
@click="warnsOpened = !warnsOpened">
|
||||
<span class="bg-gray-300 p-1 rounded font-bold">{{
|
||||
warns ? warns.length : 0
|
||||
}}</span><span>Уведомлений</span>
|
||||
</button>
|
||||
|
||||
<div v-if="warnsOpened"
|
||||
class="absolute h-fit max-h-128 w-128 overflow-auto bg-white shadow-lg rounded-lg top-32 right-4 z-50">
|
||||
<div v-if="checkWarnsExists" class="flex flex-col p-4">
|
||||
<div v-for="warn in warns">
|
||||
<div class="flex flex-row items-center gap-2 justify-between">
|
||||
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<div class="text-2xl">{{ warn.message.icon }}</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-bold">{{ warn.message.title }}</span>
|
||||
<span v-html="warn.message.body"></span>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="hideWarn(warn.id)"><i class="pi pi-times" style="font-size: 0.7rem"></i></button>
|
||||
</div>
|
||||
<Divider/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex items-center justify-center p-16">
|
||||
<button @click="fetchWarns(true)">Показать скрытые</button></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
@@ -58,6 +91,7 @@
|
||||
</div>
|
||||
<div class="flex flex-col items-center ">
|
||||
<h4 class="text-lg font-bold ">Расходы</h4>
|
||||
|
||||
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center"
|
||||
:class="totalExpenses > totalIncomes ? ' text-red-700' : ''">
|
||||
-{{ formatAmount(totalExpenses) }} ({{ formatAmount(totalExpenses - totalIncomes) }})
|
||||
@@ -65,6 +99,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
|
||||
<div class="grid grid-cols-2 !gap-1 mt-4" :class="detailedShowed ? 'block' : 'hidden'">
|
||||
<div class="flex flex-col items-center font-bold ">
|
||||
<p class="font-light ">в первый период</p>
|
||||
@@ -136,7 +172,7 @@
|
||||
<div class="flex flex-col items-center ">
|
||||
<span class="text-sm lg:text-base">Факт. траты 📛</span>
|
||||
<div class="font-light bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center">
|
||||
{{ formatAmount(totalInstantExpenses)}} ₽
|
||||
{{ formatAmount(totalInstantExpenses) }} ₽
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -144,7 +180,7 @@
|
||||
<div class="flex flex-col items-center w-full ">
|
||||
<span class="text-sm lg:text-base">Остаток на траты</span>
|
||||
<div class="font-light bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center">
|
||||
{{ formatAmount(totalInstantIncomes - totalInstantExpenses)}} ₽
|
||||
{{ formatAmount(totalInstantIncomes - totalInstantExpenses) }} ₽
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 !gap-1 mt-4" :class="detailedShowed ? 'block' : 'hidden'">
|
||||
@@ -170,8 +206,10 @@
|
||||
</div>
|
||||
<div class=" flex gap-2 overflow-x-auto ">
|
||||
|
||||
<button v-for="categorySum in transactionCategoriesSums" @click="selectCategoryType(categorySum.category.id)"
|
||||
class="rounded-xl border p-1 bg-white border-gray-300 mb-2 min-w-fit px-2" :class="selectedCategoryId == categorySum.category.id ? '!bg-blue-100' : ''">
|
||||
<button v-for="categorySum in transactionCategoriesSums"
|
||||
@click="selectCategoryType(categorySum.category.id)"
|
||||
class="rounded-xl border p-1 bg-white border-gray-300 mb-2 min-w-fit px-2"
|
||||
:class="selectedCategoryId == categorySum.category.id ? '!bg-blue-100' : ''">
|
||||
<p><span class="text-sm font-bold">{{ categorySum.category.name }}</span>: {{ categorySum.sum }} ₽</p>
|
||||
|
||||
</button>
|
||||
@@ -181,6 +219,7 @@
|
||||
:transaction="transaction"
|
||||
:is-list="true"
|
||||
@transaction-updated="updateTransactions"
|
||||
@transaction-checked="updateTransactions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -211,8 +250,9 @@
|
||||
<ul class="space-y-2">
|
||||
<!-- {{ plannedIncomes }}-->
|
||||
<BudgetTransactionView v-for="transaction in plannedIncomes" :transaction="transaction"
|
||||
:is-list="false" @transaction-checked="fetchBudgetTransactions"
|
||||
@transaction-updated="updateTransactions"/>
|
||||
:is-list="false"
|
||||
@transaction-updated="updateTransactions"
|
||||
@transaction-checked="updateTransactions"/>
|
||||
|
||||
</ul>
|
||||
|
||||
@@ -241,7 +281,7 @@
|
||||
<ul class="space-y-2">
|
||||
|
||||
<BudgetTransactionView v-for="transaction in plannedExpenses" :transaction="transaction"
|
||||
:is-list="false" @transaction-checked="fetchBudgetTransactions"
|
||||
:is-list="false" @transaction-checked="updateTransactions"
|
||||
@transaction-updated="updateTransactions"/>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -268,8 +308,10 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class=" flex gap-2 overflow-x-auto">
|
||||
<button v-for="categorySum in transactionCategoriesSums" @click="selectCategoryType(categorySum.category.id)"
|
||||
class="rounded-xl border p-1 bg-white border-gray-300 mb-2 min-w-fit px-2" :class="selectedCategoryId == categorySum.category.id ? '!bg-blue-100' : ''">
|
||||
<button v-for="categorySum in transactionCategoriesSums"
|
||||
@click="selectCategoryType(categorySum.category.id)"
|
||||
class="rounded-xl border p-1 bg-white border-gray-300 mb-2 min-w-fit px-2"
|
||||
:class="selectedCategoryId == categorySum.category.id ? '!bg-blue-100' : ''">
|
||||
<p><span class="text-sm font-bold">{{ categorySum.category.name }}</span>: {{ categorySum.sum }} ₽</p>
|
||||
|
||||
</button>
|
||||
@@ -280,6 +322,7 @@
|
||||
:is-list="true"
|
||||
@transaction-updated="updateTransactions"
|
||||
|
||||
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -288,16 +331,16 @@
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- <TransactionEditDrawer v-if="drawerOpened" :visible="drawerOpened"-->
|
||||
<!-- :transaction-type="transactionType"-->
|
||||
<!-- :category-type="categoryType"-->
|
||||
<!-- :transactions="transactions.slice(0,3)"-->
|
||||
<!-- @transaction-updated="updateTransactions"-->
|
||||
<!-- @delete-transaction="updateTransactions"-->
|
||||
<!-- @create-transaction="updateTransactions"-->
|
||||
<!-- @close-drawer="closeDrawer"-->
|
||||
<!-- <TransactionEditDrawer v-if="drawerOpened" :visible="drawerOpened"-->
|
||||
<!-- :transaction-type="transactionType"-->
|
||||
<!-- :category-type="categoryType"-->
|
||||
<!-- :transactions="transactions.slice(0,3)"-->
|
||||
<!-- @transaction-updated="updateTransactions"-->
|
||||
<!-- @delete-transaction="updateTransactions"-->
|
||||
<!-- @create-transaction="updateTransactions"-->
|
||||
<!-- @close-drawer="closeDrawer"-->
|
||||
|
||||
<!-- />-->
|
||||
<!-- />-->
|
||||
|
||||
<TransactionForm v-if="drawerOpened" :visible="drawerOpened" :transaction-type="transactionType"
|
||||
:category-type="categoryType" @close-drawer="closeDrawer" @transaction-updated="updateTransactions"
|
||||
@@ -316,9 +359,11 @@ import {
|
||||
getBudgetCategories,
|
||||
getBudgetInfo,
|
||||
getBudgetTransactions,
|
||||
updateBudgetCategoryRequest
|
||||
updateBudgetCategoryRequest,
|
||||
getWarns,
|
||||
hideWarnRequest
|
||||
} from "@/services/budgetsService";
|
||||
import {Budget, BudgetCategory, BudgetInfo} from "@/models/Budget";
|
||||
import {Budget, BudgetCategory, Warn} from "@/models/Budget";
|
||||
import {useRoute} from "vue-router";
|
||||
import {formatAmount, formatDate} from "@/utils/utils";
|
||||
import ProgressBar from "primevue/progressbar";
|
||||
@@ -331,6 +376,7 @@ import LoadingView from "@/components/LoadingView.vue";
|
||||
import ChartDataLabels from 'chartjs-plugin-datalabels';
|
||||
import {Chart as ChartJS} from 'chart.js/auto';
|
||||
import SelectButton from "primevue/selectbutton";
|
||||
import Divider from "primevue/divider";
|
||||
import TransactionForm from "@/components/transactions/TransactionForm.vue";
|
||||
|
||||
// Зарегистрируем плагин
|
||||
@@ -350,10 +396,24 @@ const modes = [
|
||||
|
||||
const value = ref(50)
|
||||
|
||||
const warnsOpened = ref(false);
|
||||
|
||||
const hideWarn = async (warnId: string) => {
|
||||
await hideWarnRequest(route.params.id, warnId)
|
||||
await fetchWarns()
|
||||
}
|
||||
|
||||
const leftForUnplanned = ref(0)
|
||||
|
||||
|
||||
const budget = ref<Budget>()
|
||||
const warns = ref<[Warn]>()
|
||||
const checkWarnsExists = computed(() => {
|
||||
console.log(warns?.value && warns.value.length > 0 ? "true" : "false");
|
||||
return warns?.value?.length > 0;
|
||||
});
|
||||
|
||||
|
||||
const plannedIncomes = ref<Transaction[]>([])
|
||||
const totalIncomes = computed(() => {
|
||||
let totalIncome = 0;
|
||||
@@ -365,7 +425,7 @@ const totalIncomes = computed(() => {
|
||||
|
||||
const totalInstantIncomes = computed(() => {
|
||||
let totalIncome = 0;
|
||||
transactions.value.filter(t => t.transactionType.code=='INSTANT' && t.category.type.code =='INCOME' ).forEach((i) => {
|
||||
transactions.value.filter(t => t.type.code == 'INSTANT' && t.category.type.code == 'INCOME').forEach((i) => {
|
||||
totalIncome += i.amount
|
||||
})
|
||||
return totalIncome
|
||||
@@ -383,7 +443,7 @@ const totalIncomeLeftToGet = computed(() => {
|
||||
|
||||
const totalLoans = computed(() => {
|
||||
let value = 0
|
||||
categories.value.filter((cat) => cat.category.id == 29).forEach(cat => {
|
||||
categories.value.filter((cat) => cat.category.id == "675850148198643f121e465d").forEach(cat => {
|
||||
value += cat.currentLimit
|
||||
})
|
||||
return value
|
||||
@@ -391,18 +451,18 @@ const totalLoans = computed(() => {
|
||||
|
||||
const loansRatio = computed(() => {
|
||||
|
||||
return totalExpenses.value == 0? 0 : totalLoans.value / totalExpenses.value * 100
|
||||
return totalExpenses.value == 0 ? 0 : totalLoans.value / totalExpenses.value * 100
|
||||
})
|
||||
|
||||
|
||||
const savingRatio = computed(() => {
|
||||
|
||||
return totalExpenses.value == 0? 0 :totalSaving.value / totalExpenses.value * 100
|
||||
return totalExpenses.value == 0 ? 0 : totalSaving.value / totalExpenses.value * 100
|
||||
})
|
||||
|
||||
const totalSaving = computed(() => {
|
||||
let value = 0
|
||||
categories.value.filter((cat) => cat.category.id == 35).forEach(cat => {
|
||||
categories.value.filter((cat) => cat.category.id == "675850148198643f121e466a").forEach(cat => {
|
||||
value += cat.currentLimit
|
||||
})
|
||||
return value
|
||||
@@ -430,7 +490,7 @@ const closeDrawer = async () => {
|
||||
}
|
||||
|
||||
const dailyRatio = computed(() => {
|
||||
const value = totalExpenses.value == 0? 0 : (totalExpenses.value - totalLoans.value - totalSaving.value) / totalExpenses.value
|
||||
const value = totalExpenses.value == 0 ? 0 : (totalExpenses.value - totalLoans.value - totalSaving.value) / totalExpenses.value
|
||||
|
||||
return value * 100
|
||||
})
|
||||
@@ -441,13 +501,13 @@ const plannedExpenses = ref<Transaction[]>([])
|
||||
const totalExpenses = computed(() => {
|
||||
let totalExpense = 0;
|
||||
categories.value.forEach((cat) => {
|
||||
let catValue = cat.currentLimit - cat.categoryPlannedLimit
|
||||
|
||||
plannedExpenses.value.filter(t => t.category.id == cat.category.id).forEach((i) => {
|
||||
|
||||
catValue += i.amount
|
||||
})
|
||||
let catValue = cat.currentLimit
|
||||
|
||||
// plannedExpenses.value.filter(t => t.category.id == cat.category.id).forEach((i) => {
|
||||
//
|
||||
// catValue += i.amount
|
||||
// })
|
||||
//
|
||||
totalExpense += catValue
|
||||
})
|
||||
|
||||
@@ -465,7 +525,7 @@ const totalPlannedExpenses = computed(() => {
|
||||
|
||||
const totalInstantExpenses = computed(() => {
|
||||
let totalExpenses = 0;
|
||||
transactions.value.filter(t => t.transactionType.code=='INSTANT' && t.category.type.code =='EXPENSE').forEach((i) => {
|
||||
transactions.value.filter(t => t.type.code == 'INSTANT' && t.category.type.code == 'EXPENSE').forEach((i) => {
|
||||
totalExpenses += i.amount
|
||||
})
|
||||
return totalExpenses
|
||||
@@ -487,14 +547,14 @@ const transactions = ref<Transaction[]>([])
|
||||
|
||||
const selectedCategoryId = ref()
|
||||
const selectCategoryType = (categoryId) => {
|
||||
if (selectedCategoryId.value==categoryId) {
|
||||
if (selectedCategoryId.value == categoryId) {
|
||||
selectedCategoryId.value = null
|
||||
} else {
|
||||
selectedCategoryId.value = categoryId
|
||||
}
|
||||
}
|
||||
const filteredTransactions = computed(() => {
|
||||
return selectedCategoryId.value ? transactions.value.filter(i => i.category.id==selectedCategoryId.value) : transactions.value
|
||||
return selectedCategoryId.value ? transactions.value.filter(i => i.category.id == selectedCategoryId.value) : transactions.value
|
||||
})
|
||||
const fetchBudgetTransactions = async () => {
|
||||
transactions.value = await getBudgetTransactions(route.params.id, 'INSTANT')
|
||||
@@ -502,7 +562,12 @@ const fetchBudgetTransactions = async () => {
|
||||
}
|
||||
|
||||
const updateTransactions = async () => {
|
||||
await Promise.all([fetchPlannedIncomes(), fetchPlannedExpenses(), fetchBudgetCategories(), fetchBudgetTransactions()])
|
||||
|
||||
setTimeout(async () => {
|
||||
await Promise.all([fetchBudgetInfo(),fetchWarns()])
|
||||
|
||||
}, 10)
|
||||
|
||||
}
|
||||
|
||||
const categories = ref<BudgetCategory[]>([])
|
||||
@@ -541,18 +606,27 @@ const transactionCategoriesSums = computed(() => {
|
||||
// }
|
||||
|
||||
|
||||
const budgetInfo = ref<BudgetInfo>();
|
||||
const budgetInfo = ref<Budget>();
|
||||
const fetchBudgetInfo = async () => {
|
||||
|
||||
|
||||
budget.value = await getBudgetInfo(route.params.id);
|
||||
plannedExpenses.value = budget.value?.plannedExpenses.copyWithin()
|
||||
plannedIncomes.value = budget.value?.plannedIncomes.copyWithin()
|
||||
transactions.value = budget.value?.transactions.copyWithin()
|
||||
categories.value = budget.value?.categories
|
||||
updateLoading.value = false
|
||||
}
|
||||
|
||||
|
||||
const updateBudgetCategory = async (category) => {
|
||||
|
||||
// loading.value = true
|
||||
await updateBudgetCategoryRequest(budget.value.id, category)
|
||||
category = await updateBudgetCategoryRequest(budget.value.id, category)
|
||||
await fetchBudgetInfo()
|
||||
setTimeout(async () => {
|
||||
await fetchWarns()
|
||||
}, 500)
|
||||
|
||||
// categories.value = await getBudgetCategories(route.params.id)
|
||||
|
||||
@@ -590,6 +664,7 @@ const incomesByPeriod = computed(() => {
|
||||
let incomesUntil25 = 0
|
||||
let incomesFrom25 = 0
|
||||
plannedIncomes.value.forEach((i) => {
|
||||
i.date = new Date(i.date)
|
||||
|
||||
if (i.date >= budget.value?.dateFrom && i.date <= twentyFour.value) {
|
||||
|
||||
@@ -606,17 +681,19 @@ const incomesByPeriod = computed(() => {
|
||||
const expensesByPeriod = computed(() => {
|
||||
let expensesUntil25 = 0
|
||||
let expensesFrom25 = 0
|
||||
let totalPlannedExpensesSum = 0
|
||||
plannedExpenses.value.forEach((i) => {
|
||||
|
||||
i.date = new Date(i.date)
|
||||
if (i.date >= budget.value?.dateFrom && i.date <= twentyFour.value) {
|
||||
expensesUntil25 += i.amount
|
||||
} else {
|
||||
expensesFrom25 += i.amount
|
||||
}
|
||||
totalPlannedExpensesSum += i.amount
|
||||
})
|
||||
categories.value.forEach((i) => {
|
||||
expensesUntil25 += (i.currentLimit - i.categoryPlannedLimit) / 2
|
||||
expensesFrom25 += (i.currentLimit - i.categoryPlannedLimit) / 2
|
||||
expensesUntil25 += (i.currentLimit - i.currentPlanned) / 2
|
||||
expensesFrom25 += (i.currentLimit - i.currentPlanned) / 2
|
||||
|
||||
})
|
||||
|
||||
@@ -760,7 +837,11 @@ const incomeExpenseChartOptions = ref({
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
const fetchWarns = async (hidden: Boolean = null) => {
|
||||
warns.value = await getWarns(route.params.id, hidden)
|
||||
}
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -768,11 +849,12 @@ onMounted(async () => {
|
||||
try {
|
||||
await Promise.all([
|
||||
fetchBudgetInfo(),
|
||||
fetchWarns()
|
||||
// budget.value = await getBudgetInfo(route.params.id),
|
||||
fetchPlannedIncomes(),
|
||||
fetchPlannedExpenses(),
|
||||
fetchBudgetCategories(),
|
||||
fetchBudgetTransactions(),
|
||||
// fetchPlannedIncomes(),
|
||||
// fetchPlannedExpenses(),
|
||||
// fetchBudgetCategories(),
|
||||
// fetchBudgetTransactions(),
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('Error during fetching data:', error);
|
||||
@@ -795,7 +877,6 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
|
||||
|
||||
.max-h-tlist {
|
||||
max-height: 1170px; /* Ограничение высоты списка */
|
||||
}
|
||||
|
||||
@@ -39,10 +39,6 @@ const props = defineProps({
|
||||
categoryType: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
transactions: {
|
||||
type: Array as () => Array<Transaction>,
|
||||
required: false
|
||||
}
|
||||
});
|
||||
|
||||
@@ -258,7 +254,7 @@ const closeDrawer = () => emit('close-drawer');
|
||||
const keyboardOpen = ref(false);
|
||||
const isMobile = ref(false);
|
||||
const userAgent = ref(null);
|
||||
const transactions = ref<Transaction[]>(props.transactions);
|
||||
const transactions = ref<Transaction[]>(null);
|
||||
// Мониторинг при монтировании
|
||||
onMounted(async () => {
|
||||
loading.value = true;
|
||||
@@ -267,10 +263,12 @@ onMounted(async () => {
|
||||
|
||||
prepareData();
|
||||
|
||||
console.log("is editing " + !isEditing.value)
|
||||
if ( !isEditing.value) {
|
||||
await getTransactions('INSTANT', 'EXPENSE',null, user.value.id ).then(transactionsResponse => transactions.value = transactionsResponse.data);
|
||||
transactions.value = transactions.value.slice(0,3)
|
||||
console.log(transactions.value.slice(0,3))
|
||||
console.log("here blyat")
|
||||
await getTransactions('INSTANT', 'EXPENSE',null, user.value.id, false, 3 )
|
||||
.then(transactionsResponse => transactions.value = transactionsResponse.data);
|
||||
|
||||
}
|
||||
loading.value = false;
|
||||
const deviceInfo = platform;
|
||||
|
||||
@@ -36,7 +36,7 @@ onMounted(async () => {
|
||||
<div v-else class="">
|
||||
<div class="flex flex-col bg-gray-200 outline outline-2 outline-gray-300 rounded-2xl p-4">
|
||||
<div class="flex flex-row items-center min-w-fit justify-between">
|
||||
<p class="text-2xl font-bold">Categories</p>
|
||||
<p class="text-2xl font-bold">Категории</p>
|
||||
<router-link to="/settings/categories">
|
||||
<Button size="large" icon="pi pi-arrow-circle-right" severity="secondary" text rounded/>
|
||||
</router-link>
|
||||
|
||||
@@ -33,7 +33,7 @@ onMounted(async () => {
|
||||
<div v-else class="">
|
||||
<div class="flex flex-col bg-gray-200 outline outline-2 outline-gray-300 rounded-2xl p-4">
|
||||
<div class="flex flex-row items-center min-w-fit justify-between">
|
||||
<p class="text-2xl font-bold">Recurrent operations</p>
|
||||
<p class="text-2xl font-bold">Повторяемые операции</p>
|
||||
<router-link to="/settings/recurrents">
|
||||
<Button size="large" icon="pi pi-arrow-circle-right" severity="secondary" text rounded/>
|
||||
</router-link>
|
||||
@@ -43,7 +43,7 @@ onMounted(async () => {
|
||||
class="flex rounded-xl border-2 bg-white shadow-xl min-w-fit max-h-fit gap-5 flex-row items-center justify-between w-full p-2">
|
||||
|
||||
|
||||
<div class="flex flex-row items-center p-x-4 gap-4">
|
||||
<div class="flex flex-row items-center p-x-4 ">
|
||||
<p class="text-6xl font-bold text-gray-700 dark:text-gray-400">{{ recurrent.category.icon }}</p>
|
||||
<div class="flex flex-col items-start justify-items-start w-full">
|
||||
<p class="font-bold">{{ recurrent.name }}</p>
|
||||
|
||||
@@ -6,7 +6,7 @@ import RecurrentSettingView from "@/components/settings/RecurrentSettingView.vue
|
||||
|
||||
<template>
|
||||
|
||||
<div class="flex flex-col h-full px-4 ">
|
||||
<div class="flex bg-gray-100 flex-col h-full px-4 gap-4 ">
|
||||
<h2 class="text-4xl font-bold ">Настройки</h2>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 items-start ">
|
||||
|
||||
@@ -3,17 +3,18 @@
|
||||
|
||||
</div>
|
||||
<div v-else class="flex flex-col bg-gray-100 dark:bg-gray-800 h-screen p-4">
|
||||
<ConfirmDialog/>
|
||||
<!-- Заголовок и кнопка добавления категории -->
|
||||
<div class="flex flex-row justify-between bg-gray-100">
|
||||
<h2 class="text-5xl font-bold">Categories</h2>
|
||||
<Button label="Add Category" icon="pi pi-plus" class="p-button-success" @click="openCreateDialog(null)"/>
|
||||
<h2 class="text-5xl font-bold">Категории</h2>
|
||||
<Button label="Добавить категорию" icon="pi pi-plus" @click="openCreateDialog(null)"/>
|
||||
</div>
|
||||
|
||||
<!-- Поле для поиска -->
|
||||
<div class="my-4 w-full">
|
||||
<span class="p-input-icon-left flex flex-row gap-2 items-center ">
|
||||
<i class="pi pi-search"></i>
|
||||
<InputText v-model="searchTerm" placeholder="Search categories..." class="w-full"/>
|
||||
<InputText v-model="searchTerm" placeholder="Поиск категорий..." class="w-full"/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +24,7 @@
|
||||
aria-labelledby="category-switch"/>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Список категорий с прокруткой для больших экранов -->
|
||||
<div class="flex">
|
||||
<div class="hidden sm:grid grid-cols-2 gap-x-10 w-full h-full justify-items-center overflow-y-auto">
|
||||
@@ -30,7 +32,7 @@
|
||||
<div class="grid h-full w-full min-w-fit overflow-y-auto">
|
||||
<div class=" gap-4 ">
|
||||
<div class="flex flex-row gap-2 ">
|
||||
<h3 class="text-2xl">Income Categories</h3>
|
||||
<h3 class="text-2xl">Поступления</h3>
|
||||
<Button icon="pi pi-plus" rounded outlined class="p-button-success" @click="openCreateDialog('INCOME')"/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -42,7 +44,7 @@
|
||||
:category="category"
|
||||
v-bind="category"
|
||||
@open-edit="openEdit"
|
||||
@delete-category="deleteCat"
|
||||
@delete-category="confirmDelete"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,7 +53,7 @@
|
||||
<div class="grid h-full w-full min-w-fit overflow-y-auto">
|
||||
<div class=" gap-4 justify-between ">
|
||||
<div class="flex flex-row gap-2">
|
||||
<h3 class="text-2xl">Expense Categories</h3>
|
||||
<h3 class="text-2xl">Расходы</h3>
|
||||
<Button icon="pi pi-plus" rounded outlined class="p-button-success !hover:bg-green-600"
|
||||
@click="openCreateDialog('EXPENSE')"/>
|
||||
</div>
|
||||
@@ -63,7 +65,7 @@
|
||||
:category="category"
|
||||
v-bind="category"
|
||||
@open-edit="openEdit"
|
||||
@delete-category="deleteCat"
|
||||
@delete-category="confirmDelete"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -79,7 +81,7 @@
|
||||
v-bind="category"
|
||||
class="mt-2"
|
||||
@open-edit="openEdit"
|
||||
@delete-category="deleteCat"
|
||||
@delete-category="confirmDelete"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -93,13 +95,15 @@
|
||||
@update:visible="closeCreateDialog"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import {computed, nextTick, onMounted, ref, watch} from 'vue';
|
||||
import Button from 'primevue/button';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import SelectButton from 'primevue/selectbutton';
|
||||
import ConfirmDialog from "primevue/confirmdialog";
|
||||
import CreateCategoryModal from './CreateCategoryModal.vue';
|
||||
import CategoryListItem from '@/components/settings/categories/CategoryListItem.vue';
|
||||
import {Category, CategoryType} from '@/models/Category';
|
||||
@@ -111,141 +115,148 @@ import {
|
||||
updateCategory
|
||||
} from "@/services/categoryService";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Button,
|
||||
InputText,
|
||||
SelectButton,
|
||||
CreateCategoryModal,
|
||||
CategoryListItem,
|
||||
},
|
||||
setup() {
|
||||
const loading = ref(true);
|
||||
const entireCategories = ref<Category[]>([]);
|
||||
const expenseCategories = ref<Category[]>([]);
|
||||
const incomeCategories = ref<Category[]>([]);
|
||||
const editingCategory = ref<Category | null>(null);
|
||||
const isDialogVisible = ref(false);
|
||||
import {useConfirm} from "primevue/useconfirm";
|
||||
import {useToast} from "primevue/usetoast";
|
||||
|
||||
const fetchCategories = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await getCategories();
|
||||
entireCategories.value = response.data
|
||||
expenseCategories.value = response.data.filter((category: Category) => category.type.code === 'EXPENSE');
|
||||
incomeCategories.value = response.data.filter((category: Category) => category.type.code === 'INCOME');
|
||||
} catch (error) {
|
||||
console.error('Error fetching categories:', error);
|
||||
}
|
||||
loading.value = false
|
||||
};
|
||||
|
||||
const categoryTypes = ref<CategoryType[]>([]);
|
||||
const selectedCategoryType = ref<CategoryType | null>(null);
|
||||
const fetchCategoryTypes = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await getCategoryTypes();
|
||||
categoryTypes.value = response.data;
|
||||
selectedCategoryType.value = categoryTypes.value.find((category: CategoryType) => category.code === 'EXPENSE');
|
||||
} catch (error) {
|
||||
console.error('Error fetching category types:', error);
|
||||
}
|
||||
loading.value = false
|
||||
};
|
||||
const loading = ref(true);
|
||||
const entireCategories = ref<Category[]>([]);
|
||||
const expenseCategories = ref<Category[]>([]);
|
||||
const incomeCategories = ref<Category[]>([]);
|
||||
const editingCategory = ref<Category | null>(null);
|
||||
const isDialogVisible = ref(false);
|
||||
const confirm = useConfirm();
|
||||
const toast = useToast();
|
||||
|
||||
const searchTerm = ref('');
|
||||
|
||||
const filteredExpenseCategories = computed(() =>
|
||||
expenseCategories.value.filter(category =>
|
||||
category.name.toLowerCase().includes(searchTerm.value.toLowerCase())
|
||||
)
|
||||
);
|
||||
|
||||
const filteredIncomeCategories = computed(() =>
|
||||
incomeCategories.value.filter(category =>
|
||||
category.name.toLowerCase().includes(searchTerm.value.toLowerCase())
|
||||
)
|
||||
);
|
||||
|
||||
const filteredCategories = computed(() => {
|
||||
if (selectedCategoryType.value?.code === 'EXPENSE') {
|
||||
return filteredExpenseCategories.value;
|
||||
} else {
|
||||
return filteredIncomeCategories.value;
|
||||
}
|
||||
});
|
||||
|
||||
const openCreateDialog = (categoryType: CategoryType | null = null) => {
|
||||
if (categoryType) {
|
||||
selectedCategoryType.value = categoryType;
|
||||
} else if (editingCategory.value) {
|
||||
selectedCategoryType.value = editingCategory.value.type;
|
||||
} else {
|
||||
selectedCategoryType.value = categoryTypes.value.find(category => category.code === 'EXPENSE');
|
||||
}
|
||||
isDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const closeCreateDialog = () => {
|
||||
isDialogVisible.value = false;
|
||||
editingCategory.value = null; // Сбрасываем категорию при закрытии
|
||||
};
|
||||
|
||||
const saveCategory = async (newCategory: Category) => {
|
||||
if (newCategory.id) {
|
||||
await updateCategory(newCategory.id, newCategory);
|
||||
} else {
|
||||
await createCategory(newCategory);
|
||||
}
|
||||
await fetchCategories()
|
||||
closeCreateDialog();
|
||||
};
|
||||
|
||||
const deleteCat = async (categoryId: number) => {
|
||||
await deleteCategory(categoryId);
|
||||
await fetchCategories();
|
||||
}
|
||||
|
||||
const openEdit = (category: Category) => {
|
||||
editingCategory.value = category;
|
||||
nextTick(() => {
|
||||
openCreateDialog(category.type); // Обновляем форму для редактирования
|
||||
});
|
||||
};
|
||||
|
||||
watch(editingCategory, (newCategory) => {
|
||||
if (newCategory) {
|
||||
selectedCategoryType.value = newCategory.type; // Обновляем тип при редактировании
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchCategories();
|
||||
await fetchCategoryTypes();
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
return {
|
||||
expenseCategories,
|
||||
incomeCategories,
|
||||
selectedCategoryType,
|
||||
categoryTypes,
|
||||
searchTerm,
|
||||
filteredExpenseCategories,
|
||||
filteredIncomeCategories,
|
||||
filteredCategories,
|
||||
isDialogVisible,
|
||||
openCreateDialog,
|
||||
closeCreateDialog,
|
||||
saveCategory,
|
||||
deleteCat,
|
||||
openEdit,
|
||||
editingCategory,
|
||||
loading,
|
||||
};
|
||||
},
|
||||
const fetchCategories = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await getCategories();
|
||||
entireCategories.value = response.data
|
||||
expenseCategories.value = response.data.filter((category: Category) => category.type.code === 'EXPENSE');
|
||||
incomeCategories.value = response.data.filter((category: Category) => category.type.code === 'INCOME');
|
||||
} catch (error) {
|
||||
console.error('Error fetching categories:', error);
|
||||
}
|
||||
loading.value = false
|
||||
};
|
||||
|
||||
const categoryTypes = ref<CategoryType[]>([]);
|
||||
const selectedCategoryType = ref<CategoryType | null>(null);
|
||||
const fetchCategoryTypes = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await getCategoryTypes();
|
||||
categoryTypes.value = response.data;
|
||||
selectedCategoryType.value = categoryTypes.value.find((category: CategoryType) => category.code === 'EXPENSE');
|
||||
} catch (error) {
|
||||
console.error('Error fetching category types:', error);
|
||||
}
|
||||
loading.value = false
|
||||
};
|
||||
|
||||
const searchTerm = ref('');
|
||||
|
||||
const filteredExpenseCategories = computed(() =>
|
||||
expenseCategories.value.filter(category =>
|
||||
category.name.toLowerCase().includes(searchTerm.value.toLowerCase())
|
||||
)
|
||||
);
|
||||
|
||||
const filteredIncomeCategories = computed(() =>
|
||||
incomeCategories.value.filter(category =>
|
||||
category.name.toLowerCase().includes(searchTerm.value.toLowerCase())
|
||||
)
|
||||
);
|
||||
|
||||
const filteredCategories = computed(() => {
|
||||
if (selectedCategoryType.value?.code === 'EXPENSE') {
|
||||
return filteredExpenseCategories.value;
|
||||
} else {
|
||||
return filteredIncomeCategories.value;
|
||||
}
|
||||
});
|
||||
|
||||
const openCreateDialog = (categoryType: CategoryType | null = null) => {
|
||||
if (categoryType) {
|
||||
selectedCategoryType.value = categoryType;
|
||||
} else if (editingCategory.value) {
|
||||
selectedCategoryType.value = editingCategory.value.type;
|
||||
} else {
|
||||
selectedCategoryType.value = categoryTypes.value.find(category => category.code === 'EXPENSE');
|
||||
}
|
||||
isDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const closeCreateDialog = () => {
|
||||
isDialogVisible.value = false;
|
||||
editingCategory.value = null; // Сбрасываем категорию при закрытии
|
||||
};
|
||||
|
||||
const saveCategory = async (newCategory: Category) => {
|
||||
if (newCategory.id) {
|
||||
await updateCategory(newCategory.id, newCategory);
|
||||
} else {
|
||||
await createCategory(newCategory);
|
||||
}
|
||||
await fetchCategories()
|
||||
closeCreateDialog();
|
||||
};
|
||||
|
||||
const confirmDelete = async (category: Category) => {
|
||||
|
||||
confirm.require({
|
||||
message: 'Вы уверены, что хотите выполнить это действие?\n Это нельзя будет отменить.\nВсе транзакции данной категории будут перенесены в категорию "Другое".\n',
|
||||
header: `Удаление категории ${category.name}`,
|
||||
icon: 'pi pi-info-circle',
|
||||
rejectLabel: 'Cancel',
|
||||
rejectProps: {
|
||||
label: 'Cancel',
|
||||
severity: 'secondary',
|
||||
outlined: true
|
||||
},
|
||||
acceptProps: {
|
||||
label: 'Delete',
|
||||
severity: 'danger'
|
||||
},
|
||||
accept: async () => {
|
||||
await deleteCategory(category.id);
|
||||
await fetchCategories();
|
||||
toast.add({severity: 'info', summary: 'Confirmed', detail: 'Record deleted', life: 3000});
|
||||
},
|
||||
reject: () => {
|
||||
toast.add({severity: 'error', summary: 'Rejected', detail: 'You have rejected', life: 3000});
|
||||
}
|
||||
});
|
||||
console.log(confirm)
|
||||
};
|
||||
|
||||
const deleteCat = async (categoryId: number) => {
|
||||
|
||||
await deleteCategory(categoryId);
|
||||
await fetchCategories();
|
||||
}
|
||||
|
||||
const openEdit = (category: Category) => {
|
||||
editingCategory.value = category;
|
||||
nextTick(() => {
|
||||
openCreateDialog(category.type); // Обновляем форму для редактирования
|
||||
});
|
||||
};
|
||||
|
||||
watch(editingCategory, (newCategory) => {
|
||||
if (newCategory) {
|
||||
selectedCategoryType.value = newCategory.type; // Обновляем тип при редактировании
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchCategories();
|
||||
await fetchCategoryTypes();
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Category } from "@/models/Category";
|
||||
import { PropType } from "vue";
|
||||
import Button from "primevue/button";
|
||||
|
||||
|
||||
// Определение входных параметров (props)
|
||||
const props = defineProps({
|
||||
category: { type: Object as PropType<Category>, required: true }
|
||||
@@ -19,7 +20,7 @@ const openEdit = () => {
|
||||
// Функция для удаления категории
|
||||
const deleteCategory = () => {
|
||||
console.log('deleteCategory ' + props.category?.id);
|
||||
emit("delete-category", props.category.id); // Использование события для удаления категории
|
||||
emit("delete-category", props.category); // Использование события для удаления категории
|
||||
};
|
||||
|
||||
</script>
|
||||
@@ -33,6 +34,7 @@ const deleteCategory = () => {
|
||||
<p class="font-light line-clamp-1">{{ category.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row items-center p-x-4 gap-2 ">
|
||||
<Button icon="pi pi-pen-to-square" rounded @click="openEdit"/>
|
||||
<Button icon="pi pi-trash" severity="danger" rounded @click="deleteCategory"/>
|
||||
|
||||
@@ -14,26 +14,26 @@
|
||||
|
||||
<!-- SelectButton для выбора типа категории -->
|
||||
<div class="flex justify-center mt-4">
|
||||
<SelectButton v-model="categoryType" :options="categoryTypes" optionLabel="name" />
|
||||
<SelectButton v-if="!isEditing" v-model="categoryType" :options="categoryTypes" optionLabel="name"/>
|
||||
</div>
|
||||
|
||||
<!-- Поля для создания/редактирования категории -->
|
||||
<label for="newCategoryName">Category Name:</label>
|
||||
<label for="newCategoryName">Название категории:</label>
|
||||
<input v-model="name" type="text" id="newCategoryName"/>
|
||||
|
||||
<label for="newCategoryDesc">Category Description:</label>
|
||||
<label for="newCategoryDesc">Описание категории:</label>
|
||||
<input v-model="description" type="text" id="newCategoryDesc"/>
|
||||
|
||||
<!-- Кнопки -->
|
||||
<div class="button-group">
|
||||
<button @click="saveCategory" class="create-category-btn">{{ isEditing ? 'Save' : 'Create' }}</button>
|
||||
<button @click="closeModal" class="close-modal-btn">Cancel</button>
|
||||
<button @click="saveCategory" class="create-category-btn">{{ isEditing ? 'Сохранить' : 'Создать' }}</button>
|
||||
<button @click="closeModal" class="close-modal-btn">Отмена</button>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {ref, watch, computed, PropType} from 'vue';
|
||||
import {ref, watch, computed, PropType} from 'vue';
|
||||
import Button from "primevue/button";
|
||||
import Dialog from "primevue/dialog";
|
||||
import SelectButton from "primevue/selectbutton";
|
||||
@@ -71,7 +71,7 @@ export default {
|
||||
}
|
||||
});
|
||||
|
||||
const emojis = ['😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂', '🙂', '🙃', '😉', '😊', '😇', '😍', '🥰', '😘'];
|
||||
const emojis = ['😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂', '🙂', '🙃', '😉', '😊', '😇', '😍', '🥰', '😘', '🍎', '🍔', '🍕', '🍣', '☕', '🍺', '🚗', '🚕', '🚴♂️', '🚆', '✈️', '⛴️', '🛒', '👗', '💍', '👟', '🛍️', '🎮', '🎥', '🎧', '🎢', '🎨', '🏠', '🛏️', '🧹', '🪴', '🍼', '🏥', '💊', '🩺', '🦷', '💳', '💰', '🏦', '🌍', '🗺️', '🏝️', '🏔️', '💻', '📚', '🖋️', '🏫'];
|
||||
|
||||
const toggleEmojiPicker = () => {
|
||||
showEmojiPicker.value = !showEmojiPicker.value;
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
<!-- Иконка категории -->
|
||||
|
||||
<!-- Информация о платеже -->
|
||||
<div class="flex flex-row">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex flex-row gap-2">
|
||||
<div class="flex items-center ">
|
||||
<span class="text-4xl">{{ payment.category.icon }}</span>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="card flex justify-center h-fit">
|
||||
<DrawerForm v-if="isDesktop" :visible="visible" :isEditing="isEditing" @close-drawer="closeDrawer" >
|
||||
<template #default>
|
||||
<TransactionFormContent :transaction="props.transaction" :transaction-type="transactionType" :category-type="categoryType" @close-drawer="closeDrawer" @create-transaction="transactionUpdated"
|
||||
<TransactionFormContent :transaction="props.transaction" :transaction-type="transactionType" :category-type="categoryType" @close-drawer="closeDrawer" @create-transaction="transactionUpdated('create')"
|
||||
@delete-transaction="transactionUpdated" @transaction-updated="transactionUpdated" />
|
||||
</template>
|
||||
</DrawerForm>
|
||||
@@ -55,7 +55,8 @@ const closeDrawer = () => {
|
||||
emit('close-drawer');
|
||||
};
|
||||
|
||||
const transactionUpdated = () => {
|
||||
const transactionUpdated = (text) => {
|
||||
console.log(text)
|
||||
emit("transaction-updated");
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import DatePicker from "primevue/datepicker";
|
||||
import FloatLabel from "primevue/floatlabel";
|
||||
import InputNumber from "primevue/inputnumber";
|
||||
import Button from "primevue/button";
|
||||
import {ref, onMounted, computed} from 'vue';
|
||||
import {ref, onMounted, computed, nextTick} from 'vue';
|
||||
import {Transaction, TransactionType} from "@/models/Transaction";
|
||||
import {CategoryType} from "@/models/Category";
|
||||
import SelectButton from "primevue/selectbutton";
|
||||
@@ -35,10 +35,6 @@ const props = defineProps({
|
||||
categoryType: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
transactions: {
|
||||
type: Array as () => Array<Transaction>,
|
||||
required: false
|
||||
}
|
||||
});
|
||||
|
||||
@@ -71,7 +67,7 @@ const categoryTypes = ref<CategoryType[]>([]);
|
||||
const transactionTypes = ref<TransactionType[]>([]);
|
||||
|
||||
const userStore = useUserStore();
|
||||
const user = computed( () => userStore.user)
|
||||
const user = computed(() => userStore.user)
|
||||
|
||||
const isReady = computed(() => !loading.value && loadingUser.value)
|
||||
|
||||
@@ -103,7 +99,7 @@ const checkForm = () => {
|
||||
amount: 'Сумма не может быть пустой или 0'
|
||||
};
|
||||
|
||||
if (!editedTransaction.value.transactionType) return showError(errorMessages.transactionType);
|
||||
if (!editedTransaction.value.type) return showError(errorMessages.transactionType);
|
||||
if (!editedTransaction.value.category) return showError(errorMessages.category);
|
||||
if (!editedTransaction.value.date) return showError(errorMessages.date);
|
||||
if (!editedTransaction.value.comment) return showError(errorMessages.comment);
|
||||
@@ -116,7 +112,7 @@ const checkForm = () => {
|
||||
const prepareData = () => {
|
||||
if (!props.transaction) {
|
||||
editedTransaction.value = new Transaction();
|
||||
editedTransaction.value.transactionType = transactionTypes.value.find(type => type.code === props.transactionType) || transactionTypes.value[0];
|
||||
editedTransaction.value.type = transactionTypes.value.find(type => type.code === props.transactionType) || transactionTypes.value[0];
|
||||
selectedCategoryType.value = categoryTypes.value.find(type => type.code === props.categoryType) || categoryTypes.value[0];
|
||||
editedTransaction.value.category = props.categoryType === 'EXPENSE' ? expenseCategories.value[0] : incomeCategories.value[0];
|
||||
editedTransaction.value.date = new Date();
|
||||
@@ -158,20 +154,29 @@ const showError = (message) => {
|
||||
};
|
||||
|
||||
// Создание транзакции
|
||||
const amountInput = ref(null);
|
||||
const createTransaction = async () => {
|
||||
if (checkForm()) {
|
||||
try {
|
||||
loading.value = true;
|
||||
if (editedTransaction.value.transactionType.code === 'INSTANT') {
|
||||
if (editedTransaction.value.type.code === 'INSTANT') {
|
||||
editedTransaction.value.isDone = true;
|
||||
}
|
||||
await createTransactionRequest(editedTransaction.value);
|
||||
toast.add({severity: 'success', summary: 'Транзакция создана!', detail: 'Транзакция создана!', life: 3000});
|
||||
|
||||
|
||||
setTimeout(async () => {
|
||||
amountInput.value.$el.querySelector('input').focus()
|
||||
}, 10)
|
||||
|
||||
|
||||
emit('create-transaction', editedTransaction.value);
|
||||
computeResult(true)
|
||||
toast.add({severity: 'success', summary: 'Успешно!', detail: 'Транзакция создана!', life: 3000});
|
||||
// computeResult(true)
|
||||
resetForm();
|
||||
} catch (error) {
|
||||
computeResult(false, error)
|
||||
// computeResult(false, error)
|
||||
toast.add({severity: 'error', summary: 'Ошибка!', detail: error.response.data["message"], life: 3000});
|
||||
console.error('Error creating transaction:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
@@ -193,9 +198,9 @@ const updateTransaction = async () => {
|
||||
// toast.add({severity: 'success', summary: 'Transaction updated!', detail: 'Транзакция обновлена!', life: 3000});
|
||||
emit('update-transaction', editedTransaction.value);
|
||||
emit('transaction-updated');
|
||||
computeResult(true)
|
||||
toast.add({severity: 'success', summary: 'Успешно!', detail: 'Транзакция создана!', life: 3000});
|
||||
} catch (error) {
|
||||
computeResult(false, error)
|
||||
toast.add({severity: 'error', summary: 'Ошибка!', detail: error.response.data["message"], life: 3000});
|
||||
console.error('Error updating transaction:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
@@ -216,10 +221,10 @@ const deleteTransaction = async () => {
|
||||
toast.add({severity: 'success', summary: 'Транзакция удалена!', detail: 'Транзакция удалена!', life: 3000});
|
||||
emit('delete-transaction', editedTransaction.value);
|
||||
closeDrawer()
|
||||
computeResult(true)
|
||||
|
||||
} catch (error) {
|
||||
computeResult(false, error)
|
||||
toast.add({severity: 'warn', summary: 'Error!', detail: 'Транзакция обновлена!', life: 3000});
|
||||
|
||||
toast.add({severity: 'error', summary: 'Ошибка!', detail: error.response.data["message"], life: 3000});
|
||||
|
||||
console.error('Error deleting transaction:', error);
|
||||
} finally {
|
||||
@@ -238,10 +243,10 @@ const resetForm = () => {
|
||||
|
||||
const dateErrorMessage = computed(() => {
|
||||
|
||||
if (editedTransaction.value.transactionType.code != 'PLANNED' && editedTransaction.value.date > new Date()) {
|
||||
if (editedTransaction.value.type.code != 'PLANNED' && editedTransaction.value.date > new Date()) {
|
||||
|
||||
return 'При мгновенных тратах дата должна быть меньше текущей!'
|
||||
} else if (editedTransaction.value.transactionType.code == 'PLANNED' && editedTransaction.value.date < new Date()) {
|
||||
} else if (editedTransaction.value.type.code == 'PLANNED' && editedTransaction.value.date < new Date()) {
|
||||
|
||||
return 'При плановых тратах дата должна быть больше текущей!'
|
||||
} else {
|
||||
@@ -255,7 +260,7 @@ const closeDrawer = () => emit('close-drawer');
|
||||
const keyboardOpen = ref(false);
|
||||
const isMobile = ref(false);
|
||||
const userAgent = ref(null);
|
||||
const transactions = ref<Transaction[]>(props.transactions);
|
||||
const transactions = ref<Transaction[]>(null);
|
||||
// Мониторинг при монтировании
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -264,167 +269,170 @@ onMounted(async () => {
|
||||
await fetchCategoriesAndTypes();
|
||||
|
||||
prepareData();
|
||||
|
||||
if ( !isEditing.value) {
|
||||
await getTransactions('INSTANT', 'EXPENSE',null, user.value.id ).then(transactionsResponse => transactions.value = transactionsResponse.data);
|
||||
transactions.value = transactions.value.slice(0,3)
|
||||
console.log(transactions.value.slice(0,3))
|
||||
console.log("is editing " + !isEditing.value)
|
||||
if (!isEditing.value) {
|
||||
console.log("is editing " + !isEditing.value)
|
||||
await getTransactions('INSTANT', 'EXPENSE', null, user.value.id, false, 3).then(transactionsResponse => transactions.value = transactionsResponse.data);
|
||||
// transactions.value = transactions.value.slice(0,3)
|
||||
console.log(transactions.value.slice(0, 3))
|
||||
}
|
||||
loading.value = false;
|
||||
const deviceInfo = platform;
|
||||
isMobile.value = deviceInfo.os.family === 'iOS' || deviceInfo.os.family === 'Android';
|
||||
|
||||
await nextTick();
|
||||
console.log('Amount Input Ref:', amountInput.value);
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<div class="card flex justify-center h-fit">
|
||||
<div v-if="result" class="absolute top-0 left-0 w-full h-full flex items-center justify-center z-50">
|
||||
<div
|
||||
class=" px-10 py-5 rounded-lg border border-gray-200 flex flex-col items-center gap-4"
|
||||
:class="isError ? 'bg-red-100' : 'bg-green-100'"
|
||||
aria-label="Custom ProgressSpinner">
|
||||
<i class="pi pi-check " :class="isError ? 'text-red-500' : 'text-green-500'" style="font-size: 2rem;"/>
|
||||
<p class="text-green-700" :class="isError ? 'text-red-500' : 'text-green-500'">{{ resultText }}</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="absolute w-full h-screen">
|
||||
<!-- Полупрозрачный белый фон -->
|
||||
<!-- <div class="absolute top-0 left-0 w-full h-full bg-white opacity-50 z-0"></div>-->
|
||||
|
||||
<!-- Спиннер поверх -->
|
||||
|
||||
<div v-if="result" class="absolute top-0 left-0 w-full h-full flex items-center justify-center z-50">
|
||||
<div
|
||||
class=" px-10 py-5 rounded-lg border border-gray-200 flex flex-col items-center gap-4"
|
||||
:class="isError ? 'bg-red-100' : 'bg-green-100'"
|
||||
aria-label="Custom ProgressSpinner">
|
||||
<i class="pi pi-check " :class="isError ? 'text-red-500' : 'text-green-500'" style="font-size: 2rem;"/>
|
||||
<p class="text-green-700" :class="isError ? 'text-red-500' : 'text-green-500'">{{ resultText }}</p>
|
||||
</div>
|
||||
|
||||
<LoadingView v-if="loading"/>
|
||||
<div v-else class=" grid gap-4 w-full ">
|
||||
<div class="relative w-full justify-center justify-items-center ">
|
||||
<div class="flex flex-col justify-items-center gap-2">
|
||||
<div class="flex flex-row gap-2">
|
||||
<Select v-if="!isEditing" v-model="editedTransaction.transactionType" :allow-empty="false"
|
||||
:options="transactionTypes"
|
||||
optionLabel="name"
|
||||
aria-labelledby="basic"
|
||||
class="justify-center"/>
|
||||
<SelectButton v-model="selectedCategoryType" :options="categoryTypes" :allow-empty="false"
|
||||
optionLabel="name"
|
||||
aria-labelledby="basic"
|
||||
@change="categoryTypeChanged" class="justify-center"/>
|
||||
</div>
|
||||
<button class="border border-gray-300 rounded-lg w-full z-40"
|
||||
@click="isCategorySelectorOpened = !isCategorySelectorOpened">
|
||||
<div class="flex flex-row items-center pe-4 py-2 ">
|
||||
<div class="flex flex-row justify-between w-full gap-4 px-4 items-center">
|
||||
<p class="text-3xl font-bold text-gray-700 dark:text-gray-400">{{
|
||||
editedTransaction.category.icon
|
||||
</div>
|
||||
<div class="absolute w-full h-screen">
|
||||
<!-- Полупрозрачный белый фон -->
|
||||
<!-- <div class="absolute top-0 left-0 w-full h-full bg-white opacity-50 z-0"></div>-->
|
||||
|
||||
<!-- Спиннер поверх -->
|
||||
|
||||
</div>
|
||||
|
||||
<LoadingView v-if="loading"/>
|
||||
<div v-else class=" grid gap-4 w-full ">
|
||||
<div class="relative w-full justify-center justify-items-center ">
|
||||
<div class="flex flex-col justify-items-center gap-2">
|
||||
<div class="flex flex-row gap-2">
|
||||
<Select v-if="!isEditing" v-model="editedTransaction.type" :allow-empty="false"
|
||||
:options="transactionTypes"
|
||||
optionLabel="name"
|
||||
aria-labelledby="basic"
|
||||
class="justify-center"/>
|
||||
<SelectButton v-model="selectedCategoryType" :options="categoryTypes" :allow-empty="false"
|
||||
optionLabel="name"
|
||||
aria-labelledby="basic"
|
||||
@change="categoryTypeChanged" class="justify-center"/>
|
||||
</div>
|
||||
<button class="border border-gray-300 rounded-lg w-full z-40"
|
||||
@click="isCategorySelectorOpened = !isCategorySelectorOpened">
|
||||
<div class="flex flex-row items-center pe-4 py-2 ">
|
||||
<div class="flex flex-row justify-between w-full gap-4 px-4 items-center">
|
||||
<p class="text-3xl font-bold text-gray-700 dark:text-gray-400">{{
|
||||
editedTransaction.category.icon
|
||||
}}</p>
|
||||
<div class="flex flex-col items-start justify-items-start justify-around w-full">
|
||||
<p class="font-bold text-start line-clamp-1">{{ editedTransaction.category.name }}</p>
|
||||
<p class="font-light line-clamp-1 items-start text-start">{{
|
||||
editedTransaction.category.description
|
||||
}}</p>
|
||||
<div class="flex flex-col items-start justify-items-start justify-around w-full">
|
||||
<p class="font-bold text-start line-clamp-1">{{ editedTransaction.category.name }}</p>
|
||||
<p class="font-light line-clamp-1 items-start text-start">{{
|
||||
editedTransaction.category.description
|
||||
}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
</div>
|
||||
<div>
|
||||
<span :class="{'rotate-90': isCategorySelectorOpened}"
|
||||
class="pi pi-angle-right transition-transform duration-300 text-5xl"/>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Анимированное открытие списка категорий -->
|
||||
<div v-show="isCategorySelectorOpened"
|
||||
class="absolute left-0 right-0 top-full overflow-y-auto z-50 border-b-4 border-x rounded-b-lg bg-white shadow-lg transition-all duration-500"
|
||||
:class="{ 'max-h-0': !isCategorySelectorOpened, 'max-h-[500px]': isCategorySelectorOpened }">
|
||||
<div class="grid grid-cols-2 mt-2">
|
||||
<button
|
||||
v-for="category in editedTransaction.category.type.code == 'EXPENSE' ? expenseCategories : incomeCategories"
|
||||
:key="category.id" class="border rounded-lg mx-2 mb-2"
|
||||
@click="selectCategory(category)">
|
||||
<div class="flex flex-row justify-between w-full px-2">
|
||||
<p class="text-4xl font-bold text-gray-700 dark:text-gray-400">{{ category.icon }}</p>
|
||||
<div class="flex flex-col items-start justify-items-start justify-around w-full">
|
||||
<p class="font-bold text-start">{{ category.name }}</p>
|
||||
<p class="font-light line-clamp-1 text-start">{{ category.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Анимированное открытие списка категорий -->
|
||||
<div v-show="isCategorySelectorOpened"
|
||||
class="absolute left-0 right-0 top-full overflow-y-auto z-50 border-b-4 border-x rounded-b-lg bg-white shadow-lg transition-all duration-500"
|
||||
:class="{ 'max-h-0': !isCategorySelectorOpened, 'max-h-[500px]': isCategorySelectorOpened }">
|
||||
<div class="grid grid-cols-2 mt-2">
|
||||
<button
|
||||
v-for="category in editedTransaction.category.type.code == 'EXPENSE' ? expenseCategories : incomeCategories"
|
||||
:key="category.id" class="border rounded-lg mx-2 mb-2"
|
||||
@click="selectCategory(category)">
|
||||
<div class="flex flex-row justify-between w-full px-2">
|
||||
<p class="text-4xl font-bold text-gray-700 dark:text-gray-400">{{ category.icon }}</p>
|
||||
<div class="flex flex-col items-start justify-items-start justify-around w-full">
|
||||
<p class="font-bold text-start">{{ category.name }}</p>
|
||||
<p class="font-light line-clamp-1 text-start">{{ category.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-4">
|
||||
</div>
|
||||
<div class="flex flex-row gap-4">
|
||||
|
||||
|
||||
<FloatLabel variant="on" class="">
|
||||
<InputNumber class=""
|
||||
:invalid="!editedTransaction.amount"
|
||||
:minFractionDigits="0"
|
||||
id="amount"
|
||||
v-model="editedTransaction.amount"
|
||||
mode="currency"
|
||||
currency="RUB"
|
||||
locale="ru-RU"
|
||||
@focus="keyboardOpen=true"
|
||||
@blur="keyboardOpen=false"
|
||||
|
||||
/>
|
||||
<label for="amount" class="">Сумма</label>
|
||||
</FloatLabel>
|
||||
|
||||
<!-- Comment Input -->
|
||||
|
||||
<FloatLabel variant="on" class="w-full">
|
||||
<label for="comment">Комментарий</label>
|
||||
<InputText class="w-full"
|
||||
:invalid="!editedTransaction.comment"
|
||||
id="comment"
|
||||
v-model="editedTransaction.comment"
|
||||
<FloatLabel variant="on" class="">
|
||||
<InputNumber class=""
|
||||
ref="amountInput"
|
||||
:invalid="!editedTransaction.amount"
|
||||
:minFractionDigits="0"
|
||||
id="amount"
|
||||
v-model="editedTransaction.amount"
|
||||
mode="currency"
|
||||
currency="RUB"
|
||||
locale="ru-RU"
|
||||
@focus="keyboardOpen=true"
|
||||
@blur="keyboardOpen=false"
|
||||
/>
|
||||
</FloatLabel>
|
||||
|
||||
</div>
|
||||
/>
|
||||
<label for="amount" class="">Сумма</label>
|
||||
</FloatLabel>
|
||||
|
||||
<!-- Date Picker -->
|
||||
<div class="field col-12 gap-0">
|
||||
<FloatLabel variant="on">
|
||||
<label for="date">Дата</label>
|
||||
<!-- Comment Input -->
|
||||
|
||||
<DatePicker class="w-full"
|
||||
inline
|
||||
:invalid="editedTransaction.transactionType.code != 'PLANNED' ? editedTransaction.date > new Date() : true"
|
||||
id="date"
|
||||
v-model="editedTransaction.date"
|
||||
dateFormat="yy-mm-dd"
|
||||
showIcon
|
||||
|
||||
/>
|
||||
<p :class="dateErrorMessage != '' ? 'visible' : 'invisible'"
|
||||
class="text-red-400">{{ dateErrorMessage }}</p>
|
||||
|
||||
|
||||
</FloatLabel>
|
||||
</div>
|
||||
<div>
|
||||
<BudgetTransactionView v-if="!isEditing && transactions" v-for="transaction in transactions" :is-list="true"
|
||||
:transaction="transaction"/>
|
||||
</div>
|
||||
|
||||
<div class="fixed col-12 flex justify-content-end gap-4 bottom-8">
|
||||
|
||||
<Button label="Сохранить" icon="pi pi-check" class="p-button-success"
|
||||
@click="isEditing ? updateTransaction() : createTransaction()"/>
|
||||
<Button label="Отмена" icon="pi pi-times" class="p-button-secondary " @click="closeDrawer"/>
|
||||
<Button v-if="isEditing" label="Удалить" icon="pi pi-times" class="p-button-success" severity="danger"
|
||||
@click="deleteTransaction"/>
|
||||
</div>
|
||||
<FloatLabel variant="on" class="w-full">
|
||||
<label for="comment">Комментарий</label>
|
||||
<InputText class="w-full"
|
||||
:invalid="!editedTransaction.comment"
|
||||
id="comment"
|
||||
v-model="editedTransaction.comment"
|
||||
@focus="keyboardOpen=true"
|
||||
@blur="keyboardOpen=false"
|
||||
/>
|
||||
</FloatLabel>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Date Picker -->
|
||||
<div class="field col-12 gap-0">
|
||||
<FloatLabel variant="on">
|
||||
<label for="date">Дата</label>
|
||||
|
||||
<DatePicker class="w-full"
|
||||
inline
|
||||
:invalid="editedTransaction.type.code != 'PLANNED' ? editedTransaction.date > new Date() : true"
|
||||
id="date"
|
||||
v-model="editedTransaction.date"
|
||||
dateFormat="yy-mm-dd"
|
||||
showIcon
|
||||
|
||||
/>
|
||||
<p :class="dateErrorMessage != '' ? 'visible' : 'invisible'"
|
||||
class="text-red-400">{{ dateErrorMessage }}</p>
|
||||
|
||||
|
||||
</FloatLabel>
|
||||
</div>
|
||||
<div>
|
||||
<BudgetTransactionView v-if="!isEditing && transactions" v-for="transaction in transactions" :is-list="true"
|
||||
:transaction="transaction"/>
|
||||
</div>
|
||||
|
||||
<div class="fixed col-12 flex justify-content-end gap-4 bottom-8">
|
||||
|
||||
<Button label="Сохранить" icon="pi pi-check" class="p-button-success"
|
||||
@click="isEditing ? updateTransaction() : createTransaction()"/>
|
||||
<Button label="Отмена" icon="pi pi-times" class="p-button-secondary " @click="closeDrawer"/>
|
||||
<Button v-if="isEditing" label="Удалить" icon="pi pi-times" class="p-button-success" severity="danger"
|
||||
@click="deleteTransaction"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import {computed, onMounted, ref} from "vue";
|
||||
import BudgetTransactionView from "@/components/budgets/BudgetTransactionView.vue";
|
||||
import IconField from "primevue/iconfield";
|
||||
import InputIcon from "primevue/inputicon";
|
||||
import InputText from "primevue/inputtext";
|
||||
import { getTransactions } from "@/services/transactionService";
|
||||
import { Transaction } from "@/models/Transaction";
|
||||
|
||||
import {getTransactions} from "@/services/transactionService";
|
||||
import {Transaction} from "@/models/Transaction";
|
||||
import ProgressSpinner from "primevue/progressspinner";
|
||||
import {getUsers} from "@/services/userService";
|
||||
import Button from "primevue/button";
|
||||
|
||||
const loading = ref(false);
|
||||
const searchText = ref("");
|
||||
@@ -16,12 +19,15 @@ const offset = ref(0); // Начальное смещение
|
||||
const allLoaded = ref(false); // Флаг для отслеживания окончания данных
|
||||
|
||||
// Функция для получения транзакций с параметрами limit и offset
|
||||
const fetchTransactions = async () => {
|
||||
if (loading.value || allLoaded.value) return; // Останавливаем загрузку, если уже загружается или данные загружены полностью
|
||||
const fetchTransactions = async (reload) => {
|
||||
console.log("here")
|
||||
console.log(allLoaded.value)
|
||||
// if (loading.value || allLoaded.value) return; // Останавливаем загрузку, если уже загружается или данные загружены полностью
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const response = await getTransactions('INSTANT', null,null, null, limit, offset.value);
|
||||
console.log(reload);
|
||||
const response = await getTransactions('INSTANT', null, null, selectedUserId.value ? selectedUserId.value : null, null, reload ? offset.value : limit, reload ? 0 : offset.value);
|
||||
const newTransactions = response.data;
|
||||
|
||||
// Проверка на конец данных
|
||||
@@ -30,7 +36,8 @@ const fetchTransactions = async () => {
|
||||
}
|
||||
|
||||
// Добавляем новые транзакции к текущему списку
|
||||
transactions.value.push(...newTransactions);
|
||||
|
||||
reload ? transactions.value = newTransactions : transactions.value.push(...newTransactions)
|
||||
offset.value += limit; // Обновляем смещение для следующей загрузки
|
||||
} catch (error) {
|
||||
console.error("Error fetching transactions:", error);
|
||||
@@ -38,6 +45,19 @@ const fetchTransactions = async () => {
|
||||
|
||||
loading.value = false;
|
||||
};
|
||||
const switchUserFilter = async (user) => {
|
||||
if (selectedUserId.value == user.id) {
|
||||
selectedUserId.value = null
|
||||
} else if (selectedUserId.value == null) {
|
||||
selectedUserId.value = user.id;
|
||||
} else {
|
||||
selectedUserId.value = user.id;
|
||||
}
|
||||
|
||||
await getTransactions('INSTANT', null, null, selectedUserId.value, null, offset.value, 0)
|
||||
.then(it => transactions.value = it.data)
|
||||
|
||||
}
|
||||
|
||||
const tgname = computed(() => {
|
||||
if (window.Telegram.WebApp) {
|
||||
@@ -48,30 +68,45 @@ const tgname = computed(() => {
|
||||
|
||||
// Отфильтрованные транзакции по поисковому запросу
|
||||
const filteredTransactions = computed(() => {
|
||||
if (searchText.value.length === 0) {
|
||||
// Проверяем, есть ли текст поиска
|
||||
const search = searchText.value.trim().toLowerCase();
|
||||
|
||||
if (!search) {
|
||||
// Если текст поиска пуст, возвращаем все транзакции
|
||||
return transactions.value;
|
||||
} else {
|
||||
return transactions.value.filter(transaction => {
|
||||
const search = searchText.value.toLowerCase();
|
||||
return (
|
||||
transaction.transaction.comment.toLowerCase().includes(search) ||
|
||||
transaction.transaction.category.name.toLowerCase().includes(search)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Проверяем наличие данных
|
||||
if (!transactions.value || !Array.isArray(transactions.value)) {
|
||||
console.warn("Transactions is not a valid array");
|
||||
return [];
|
||||
}
|
||||
|
||||
// Фильтруем транзакции по тексту поиска
|
||||
return transactions.value.filter(transaction => {
|
||||
return transaction.comment.toLowerCase().includes(search) ||
|
||||
transaction.category.name.toLowerCase().includes(search);
|
||||
});
|
||||
});
|
||||
|
||||
// Обработчик прокрутки для ленивой загрузки
|
||||
const handleScroll = () => {
|
||||
const bottomReached = window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 2000;
|
||||
if (bottomReached && !loading.value) {
|
||||
fetchTransactions(); // Загружаем следующую страницу
|
||||
}
|
||||
};
|
||||
|
||||
// Обработчик прокрутки для ленивой загрузки
|
||||
// const handleScroll = () => {
|
||||
// const bottomReached = window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 2000;
|
||||
// if (bottomReached && !loading.value) {
|
||||
// fetchTransactions(); // Загружаем следующую страницу
|
||||
// }
|
||||
// };
|
||||
|
||||
const users = ref([])
|
||||
const selectedUserId = ref(null)
|
||||
const fetchUsers = async () => {
|
||||
users.value = await getUsers();
|
||||
}
|
||||
onMounted(async () => {
|
||||
await fetchTransactions(); // Первоначальная загрузка данных
|
||||
window.addEventListener("scroll", handleScroll); // Добавляем обработчик прокрутки
|
||||
await fetchUsers();
|
||||
// window.addEventListener("scroll", handleScroll); // Добавляем обработчик прокрутки
|
||||
});
|
||||
|
||||
</script>
|
||||
@@ -84,20 +119,32 @@ onMounted(async () => {
|
||||
<InputIcon class="pi pi-search"/>
|
||||
<InputText v-model="searchText" placeholder="Search"></InputText>
|
||||
</IconField>
|
||||
|
||||
<div class="flex flex-row gap-2">
|
||||
<!-- <span v-for="user in users">{{user.id}}</span>-->
|
||||
<button v-for="user in users" @click="switchUserFilter(user)"
|
||||
class="rounded-xl border p-1 bg-white border-gray-300 mb-2 min-w-fit px-2"
|
||||
:class="selectedUserId == user.id ? '!bg-blue-100' : ''">
|
||||
<p><span class="text-sm font-bold">{{ user.firstName }}</span></p>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
|
||||
<BudgetTransactionView
|
||||
v-for="transaction in filteredTransactions"
|
||||
:key="transaction.id"
|
||||
:transaction="transaction"
|
||||
:is-list="true"
|
||||
@transaction-updated="fetchTransactions"
|
||||
@transaction-updated="fetchTransactions(true)"
|
||||
@delete-transaction="fetchTransactions(true)"
|
||||
/>
|
||||
<div class="flex items-center justify-center px-2 py-1 mb-5">
|
||||
<Button @click="fetchTransactions(false)">Загрузить следующие...</Button>
|
||||
</div>
|
||||
<!-- Показать спиннер загрузки, если идет загрузка -->
|
||||
<ProgressSpinner v-if="loading" class="mb-4" style="width: 50px; height: 50px;"
|
||||
<ProgressSpinner v-if="loading" class="mb-4" style="width: 50px; height: 50px;"
|
||||
strokeWidth="8"
|
||||
fill="transparent"
|
||||
animationDuration=".5s" />
|
||||
animationDuration=".5s"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,11 +10,13 @@ import Ripple from "primevue/ripple";
|
||||
import ToastService from 'primevue/toastservice'
|
||||
import Tooltip from 'primevue/tooltip';
|
||||
import { createPinia } from 'pinia';
|
||||
import ConfirmationService from 'primevue/confirmationservice';
|
||||
|
||||
|
||||
const app = createApp(App);
|
||||
app.use(router);
|
||||
app.use(ToastService);
|
||||
app.use(ConfirmationService);
|
||||
app.use(createPinia())
|
||||
app.directive('ripple', Ripple);
|
||||
app.directive('tooltip', Tooltip);
|
||||
@@ -24,6 +26,7 @@ app.use(PrimeVue, {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
app.config.globalProperties.$primevue.config.locale = {
|
||||
firstDayOfWeek: 1, // Устанавливаем понедельник как первый день недели
|
||||
dayNames: ["Воскресенье", "Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота"],
|
||||
|
||||
@@ -1,20 +1,27 @@
|
||||
import {Transaction} from "@/models/Transaction";
|
||||
import {Category, CategorySetting} from "@/models/Category";
|
||||
import {Category} from "@/models/Category";
|
||||
|
||||
export class BudgetInfo {
|
||||
budget: Budget
|
||||
export class Budget {
|
||||
id: number
|
||||
name: string
|
||||
dateFrom: Date
|
||||
dateTo: Date
|
||||
createdAt: Date
|
||||
plannedExpenses: [Transaction]
|
||||
plannedIncomes: [Transaction]
|
||||
categories: [BudgetCategory]
|
||||
transactions: [Transaction]
|
||||
transactionCategoriesSums: []
|
||||
totalIncomes: number
|
||||
totalExpenses: number
|
||||
chartData: [[]]
|
||||
unplannedCategories: [BudgetCategory]
|
||||
warns: [Warn]
|
||||
}
|
||||
|
||||
|
||||
export class Budget {
|
||||
export class Warn {
|
||||
id: string
|
||||
severity: string
|
||||
message: string
|
||||
}
|
||||
|
||||
export class BudgetInfo {
|
||||
id: number
|
||||
name: string
|
||||
dateFrom: Date
|
||||
@@ -23,6 +30,8 @@ export class Budget {
|
||||
}
|
||||
|
||||
export class BudgetCategory {
|
||||
category: Category;
|
||||
categorySetting: CategorySetting
|
||||
category: Category
|
||||
currentSpent: BigDecimal // отображает сумму потраченных на данный момент средств по категории
|
||||
currentLimit: BigDecimal // отображает текущий лимит по категории
|
||||
currentPlanned: BigDecimal // отображает текущую сумму запланированных расходов по категории
|
||||
}
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import {Category} from "@/models/Category";
|
||||
import {User} from "@/models/User";
|
||||
|
||||
export class Transaction {
|
||||
id: number;
|
||||
transactionType: TransactionType;
|
||||
id: String;
|
||||
type: TransactionType;
|
||||
user: User
|
||||
category: Category;
|
||||
comment: string;
|
||||
date: Date;
|
||||
amount: number
|
||||
parentId: String
|
||||
isDone: boolean;
|
||||
}
|
||||
|
||||
export class TransactionCategoriesSum{
|
||||
export class TransactionCategoriesSum {
|
||||
category: Category;
|
||||
sum: number
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export class TransactionType {
|
||||
code: string;
|
||||
name: string;
|
||||
|
||||
@@ -4,8 +4,8 @@ import { useRouter } from 'vue-router';
|
||||
|
||||
// Создаем экземпляр axios
|
||||
const api = axios.create({
|
||||
baseURL: 'https://luminic.space/api/v1',
|
||||
// baseURL: 'http://localhost:8000/api/v1',
|
||||
baseURL: 'https://luminic.space/api/',
|
||||
// baseURL: 'http://localhost:8082/api',
|
||||
});
|
||||
|
||||
// Устанавливаем токен из localStorage при каждом запуске
|
||||
@@ -19,7 +19,7 @@ api.interceptors.response.use(
|
||||
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response && error.response.status === 401) {
|
||||
if (error.response && error.response.status === 403) {
|
||||
localStorage.removeItem('token');
|
||||
const router = useRouter();
|
||||
router.push('/login');
|
||||
|
||||
@@ -4,25 +4,32 @@ import {format} from "date-fns";
|
||||
// Импортируете настроенный экземпляр axios
|
||||
|
||||
export const getBudgetInfos = async () => {
|
||||
try {
|
||||
|
||||
let response = await apiClient.get('/budgets/');
|
||||
|
||||
let response = await apiClient.get('/budgets');
|
||||
let budgetInfos = response.data;
|
||||
budgetInfos.forEach((budgetInfo: Budget) => {
|
||||
budgetInfo.dateFrom = new Date(budgetInfo.dateFrom);
|
||||
budgetInfo.dateTo = new Date(budgetInfo.dateTo);
|
||||
// budgetInfo.plannedExpenses.forEach(e => {
|
||||
// e.date = new Date(e.date)
|
||||
// })
|
||||
//
|
||||
// budgetInfo.plannedIncomes.forEach(e => {
|
||||
// e.date = new Date(e.date)
|
||||
// })
|
||||
//
|
||||
// budgetInfo.transactions.forEach(e => {
|
||||
// e.date = new Date(e.date)
|
||||
// })
|
||||
budgetInfo.plannedExpenses?.forEach(e => {
|
||||
e.date = new Date(e.date)
|
||||
})
|
||||
|
||||
budgetInfo.plannedIncomes?.forEach(e => {
|
||||
e.date = new Date(e.date)
|
||||
})
|
||||
|
||||
budgetInfo.transactions?.forEach(e => {
|
||||
e.date = new Date(e.date)
|
||||
})
|
||||
})
|
||||
return budgetInfos
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
throw e
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export const getBudgetTransactions = async (budgetId, transactionType, categoryType) => {
|
||||
@@ -66,18 +73,50 @@ export const getBudgetInfo = async (budget_id: number) => {
|
||||
|
||||
};
|
||||
|
||||
|
||||
export const getWarns = async (budgetId: string, hidden: Boolean = null) => {
|
||||
let url = `/budgets/${budgetId}/warns`
|
||||
if (hidden) {
|
||||
url += `?hidden=${hidden}`
|
||||
}
|
||||
let warns = await apiClient.get(url);
|
||||
return warns.data
|
||||
}
|
||||
|
||||
export const hideWarnRequest = async (budgetId: string, warnId: string) => {
|
||||
await apiClient.post(`/budgets/${budgetId}/warns/${warnId}/hide`);
|
||||
}
|
||||
|
||||
export const updateBudgetCategoryRequest = async (budget_id, category: BudgetCategory) => {
|
||||
await apiClient.put('/budgets/' + budget_id + '/category', category);
|
||||
return await apiClient.post('/budgets/' + budget_id + '/categories/' + category.category.id + "/limit", {"limit": category.currentLimit}).then(i => i.data);
|
||||
}
|
||||
|
||||
export const createBudget = async (budget: Budget, createRecurrent: Boolean) => {
|
||||
budget.dateFrom = format(budget.dateFrom, 'yyyy-MM-dd')
|
||||
budget.dateTo = format(budget.dateTo, 'yyyy-MM-dd')
|
||||
let data = {
|
||||
budget: budget,
|
||||
createRecurrent: createRecurrent
|
||||
|
||||
try {
|
||||
|
||||
let budgetToCreate = JSON.parse(JSON.stringify(budget));
|
||||
budgetToCreate.dateFrom = format(budget.dateFrom, 'yyyy-MM-dd')
|
||||
budgetToCreate.dateTo = format(budget.dateTo, 'yyyy-MM-dd')
|
||||
let data = {
|
||||
budget: budgetToCreate,
|
||||
createRecurrent: createRecurrent
|
||||
}
|
||||
await apiClient.post('/budgets/', data);
|
||||
|
||||
} catch (e){
|
||||
console.error(e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const deleteBudgetRequest = async (budgetId: string) => {
|
||||
try {
|
||||
// throw Error("test")
|
||||
let response = await apiClient.delete(`/budgets/${budgetId}`);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw error;
|
||||
}
|
||||
await apiClient.post('/budgets', data);
|
||||
budget.dateFrom = format(budget.dateFrom, 'dd.mm.yy')
|
||||
budget.dateTo = format(budget.dateTo, 'dd.mm.yy')
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import {Category} from "@/models/Category"; // Импортируете нас
|
||||
export const getCategories = async (type = null) => {
|
||||
|
||||
type = type ? type : ''
|
||||
return await apiClient.get('/categories/?type=' + type);
|
||||
return await apiClient.get('/categories?type=' + type);
|
||||
};
|
||||
|
||||
export const getCategoryTypes = async () => {
|
||||
|
||||
@@ -7,9 +7,10 @@ export const getTransaction = async (transactionId: int) => {
|
||||
return await apiClient.post(`/transactions/${transactionId}`,);
|
||||
}
|
||||
|
||||
export const getTransactions = async (transaction_type = null, category_type = null, category_id = null, user_id = null, limit = null, offset = null) => {
|
||||
export const getTransactions = async (transaction_type = null, category_type = null, category_id = null, user_id = null, is_child = null, limit = null, offset = null) => {
|
||||
const params = {};
|
||||
|
||||
console.log(is_child)
|
||||
// Add the parameters to the params object if they are not null
|
||||
if (transaction_type) {
|
||||
params.transaction_type = transaction_type;
|
||||
@@ -25,6 +26,9 @@ export const getTransactions = async (transaction_type = null, category_type = n
|
||||
if (user_id) {
|
||||
params.user_id = user_id
|
||||
}
|
||||
if (is_child!=null){
|
||||
params.is_child = is_child
|
||||
}
|
||||
if (limit) {
|
||||
params.limit = limit
|
||||
}
|
||||
@@ -33,22 +37,25 @@ export const getTransactions = async (transaction_type = null, category_type = n
|
||||
}
|
||||
|
||||
// Use axios to make the GET request, passing the params as the second argument
|
||||
return await apiClient.get('/transactions/', {
|
||||
return await apiClient.get('/transactions', {
|
||||
params: params
|
||||
});
|
||||
}
|
||||
|
||||
export const createTransactionRequest = async (transaction: Transaction) => {
|
||||
transaction.date = format(transaction.date, 'yyyy-MM-dd')
|
||||
let transactionResponse = await apiClient.post('/transactions', transaction);
|
||||
console.log(transaction.date)
|
||||
let transactionResponse = await apiClient.post('/transactions', transaction);
|
||||
|
||||
transaction.date = new Date(transaction.date);
|
||||
return transactionResponse.data
|
||||
};
|
||||
|
||||
export const updateTransactionRequest = async (transaction: Transaction) => {
|
||||
const id = transaction.id
|
||||
transaction.date = format(transaction.date, 'yyyy-MM-dd')
|
||||
// console.log(transaction.isDone)
|
||||
// transaction.date = transaction.date.setHours(0,0,0,0)
|
||||
transaction.date = format(transaction.date, "yyyy-MM-dd")
|
||||
|
||||
const response = await apiClient.put(`/transactions/${id}`, transaction);
|
||||
transaction = response.data
|
||||
transaction.date = new Date(transaction.date);
|
||||
@@ -56,15 +63,15 @@ export const updateTransactionRequest = async (transaction: Transaction) => {
|
||||
return transaction
|
||||
};
|
||||
|
||||
export const setTransactionDoneRequest = async (transaction: Transaction) => {
|
||||
const id = transaction.id
|
||||
// transaction.date = format(transaction.date, 'yyyy-MM-dd')
|
||||
const response = await apiClient.put(`/transactions/${id}/done`, transaction);
|
||||
transaction = response.data
|
||||
transaction.date = new Date(transaction.date);
|
||||
|
||||
return transaction
|
||||
};
|
||||
// export const setTransactionDoneRequest = async (transaction: Transaction) => {
|
||||
// const id = transaction.id
|
||||
// // transaction.date = format(transaction.date, 'yyyy-MM-dd')
|
||||
// const response = await apiClient.patch(`/transactions/${id}/set-done`, transaction);
|
||||
// // transaction = response.data
|
||||
// // transaction.date = new Date(transaction.date);
|
||||
//
|
||||
// // return transaction
|
||||
// };
|
||||
|
||||
export const deleteTransactionRequest = async (id: number) => {
|
||||
return await apiClient.delete(`/transactions/${id}`);
|
||||
@@ -74,7 +81,7 @@ export const getTransactionTypes = async () => {
|
||||
return await apiClient.get('/transactions/types');
|
||||
}
|
||||
|
||||
export const getTransactionCategoriesSums = async () => {
|
||||
export const getTransactionCategoriesSums = async () => {
|
||||
let response = await apiClient.get('/transactions/categories/_calc_sums');
|
||||
return response.data;
|
||||
}
|
||||
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 { ref } from 'vue';
|
||||
import {defineStore} from 'pinia';
|
||||
import {ref} from 'vue';
|
||||
import apiClient from "@/services/axiosSetup";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
const user = ref(null);
|
||||
const loadingUser = ref(true);
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
async function fetchUserProfile() {
|
||||
// Убираем проверку на `loadingUser`, чтобы не блокировать запрос
|
||||
if (!user.value) {
|
||||
loadingUser.value = true;
|
||||
try {
|
||||
const response = await apiClient.get('/auth/users/me'); // запрос к API для получения данных пользователя
|
||||
const response = await apiClient.get('/auth/me'); // запрос к API для получения данных пользователя
|
||||
if (response.status !== 200) throw new Error('Ошибка загрузки данных пользователя');
|
||||
|
||||
user.value = response.data;
|
||||
@@ -24,5 +27,29 @@ export const useUserStore = defineStore('user', () => {
|
||||
}
|
||||
}
|
||||
|
||||
return { user, loadingUser, fetchUserProfile };
|
||||
// Основная функция для логина
|
||||
async function login( username, password, tg_id=null) {
|
||||
try {
|
||||
let response;
|
||||
if (tg_id) {
|
||||
response = await apiClient.post('/auth/token/tg', {tg_id: tg_id});
|
||||
} else {
|
||||
response = await apiClient.post('/auth/login', {
|
||||
username: username,
|
||||
password: password,
|
||||
});
|
||||
}
|
||||
|
||||
const token = response.data.token;
|
||||
localStorage.setItem('token', token);
|
||||
apiClient.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
||||
await fetchUserProfile();
|
||||
await router.push(route.query['back'] ? route.query['back'].toString() : '/');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert('Ошибка входа. Проверьте логин и пароль.');
|
||||
}
|
||||
};
|
||||
|
||||
return {user, loadingUser, fetchUserProfile, login};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user