tags and new analytics new in budget
This commit is contained in:
11
src/App.vue
11
src/App.vue
@@ -15,17 +15,17 @@
|
|||||||
<div class="bg-gray-100 h-12 block lg:hidden"></div>
|
<div class="bg-gray-100 h-12 block lg:hidden"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="footer" class="flex flex-col w-full h-fit bg-gray-200 p-4 gap-4">
|
<div id="footer" class="flex flex-col w-full h-fit bg-gray-200 p-4 gap-4 ">
|
||||||
<div class="flex flex-row items-center gap-6 ">
|
<div class="flex flex-row items-start gap-2 ">
|
||||||
<div class="flex flex-row items-center gap-2 min-w-fit">
|
<div class="flex flex-row items-center gap-2 min-w-fit">
|
||||||
<img alt="logo" src="/apple-touch-icon.png" width="48" height="48"/>
|
<img alt="logo" src="/apple-touch-icon.png" width="48" height="48"/>
|
||||||
<div class="flex flex-col items-start">
|
<div class="flex flex-col items-start">
|
||||||
<p class="text-lg font-bold">Luminic Space</p>
|
<p class="text-md font-bold">Luminic Space</p>
|
||||||
<p>Ваше пространство</p>
|
<p class="text-sm">Ваше пространство</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col sm:flex-row w-full gap-4">
|
<div class="grid grid-cols-2 sm:flex sm:flex-row w-full gap-1">
|
||||||
<router-link to="/about" class="hover:underline">О проекте</router-link>
|
<router-link to="/about" class="hover:underline">О проекте</router-link>
|
||||||
<router-link to="/spaces" class="hover:underline">Пространства</router-link>
|
<router-link to="/spaces" class="hover:underline">Пространства</router-link>
|
||||||
<router-link to="/analytics" class="hover:underline">Аналитика</router-link>
|
<router-link to="/analytics" class="hover:underline">Аналитика</router-link>
|
||||||
@@ -40,6 +40,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>v0.0.2</div>
|
<div>v0.0.2</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="h-16 lg:h-0"/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
--vt-c-black: #181818;
|
--vt-c-black: #181818;
|
||||||
--vt-c-black-soft: #222222;
|
--vt-c-black-soft: #222222;
|
||||||
--vt-c-black-mute: #282828;
|
--vt-c-black-mute: #282828;
|
||||||
|
--p-selectbutton-border-radius: 10px;
|
||||||
|
|
||||||
--vt-c-indigo: #2c3e50;
|
--vt-c-indigo: #2c3e50;
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,11 @@
|
|||||||
height: 0.5rem !important;
|
height: 0.5rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.p-selectbutton{
|
||||||
|
border-width: 1px !important;
|
||||||
|
border-color: #d1d5db !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
canvas {
|
canvas {
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ const items = ref([
|
|||||||
url: '/settings',
|
url: '/settings',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Создать',
|
label: 'Запись',
|
||||||
icon: 'pi pi-plus',
|
icon: 'pi pi-plus',
|
||||||
url:'',
|
url:'',
|
||||||
command: () => {
|
command: () => {
|
||||||
|
|||||||
@@ -2,45 +2,54 @@
|
|||||||
<div
|
<div
|
||||||
class=" items-center toolbar-example justify-between bg-white outline rounded-xl outline-gray-300 shadow-lg h-fit fixed"
|
class=" items-center toolbar-example justify-between bg-white outline rounded-xl outline-gray-300 shadow-lg h-fit fixed"
|
||||||
style="width: 90%; left:5%; bottom: 1.5rem;">
|
style="width: 90%; left:5%; bottom: 1.5rem;">
|
||||||
<!-- <TransactionForm v-if="drawerOpened" :visible="drawerOpened" :transaction-type="'INSTANT'"-->
|
<!-- <TransactionForm v-if="drawerOpened" :visible="drawerOpened" :transaction-type="'INSTANT'"-->
|
||||||
<!-- :category-type="'EXPENSE'" @close-drawer="closeDrawer"/>-->
|
<!-- :category-type="'EXPENSE'" @close-drawer="closeDrawer"/>-->
|
||||||
<div class="flex flex-row rounded-full px-2 justify-between overflow-x">
|
<div class="flex flex-row rounded-full px-2 justify-between overflow-x-scroll">
|
||||||
<!-- <div class="flex flex-col gap-2 p-2">-->
|
<div class=" flex-col gap-2 p-2 sm:flex">
|
||||||
<!-- <router-link to="/spaces" class="items-center flex flex-col gap-2">-->
|
<button class="items-center flex flex-col gap-2" @click="openDrawer('INSTANT', 'EXPENSE')">
|
||||||
<!-- <i class="pi pi-compass text-2xl" style="font-size: 1.5rem"></i>-->
|
<i class="pi pi-plus text-xl" style="font-size: 1rem"></i>
|
||||||
<!-- <p>Пространства</p>-->
|
<p>Запись</p>
|
||||||
<!-- </router-link>-->
|
</button>
|
||||||
<!-- </div>-->
|
</div>
|
||||||
<div class="flex flex-col gap-2 p-2">
|
<div class="flex flex-col gap-2 p-2">
|
||||||
<router-link to="/analytics" class="items-center flex flex-col gap-2">
|
<router-link to="/spaces" class="items-center flex flex-col gap-2">
|
||||||
<i class="pi pi-chart-line text-2xl" style="font-size: 1.5rem"></i>
|
<i class="pi pi-compass text-2xl" style="font-size: 1rem"></i>
|
||||||
<p>Аналитика</p>
|
<p>Пространства</p>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-2 p-2">
|
<div class="flex flex-col gap-2 p-2">
|
||||||
<router-link to="/budgets" class="items-center flex flex-col gap-2">
|
<router-link to="/budgets" class="items-center flex flex-col gap-2">
|
||||||
<i class="pi pi-briefcase text-2xl" style="font-size: 1.5rem"></i>
|
<i class="pi pi-briefcase text-2xl" style="font-size: 1rem"></i>
|
||||||
<p>Бюджеты</p>
|
<p>Бюджеты</p>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-col gap-2 p-2">
|
||||||
|
<router-link to="/analytics" class="items-center flex flex-col gap-2">
|
||||||
|
<i class="pi pi-chart-line text-2xl" style="font-size: 1rem"></i>
|
||||||
|
<p>Аналитика</p>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2 p-2">
|
<div class="flex flex-col gap-2 p-2">
|
||||||
<router-link to="/transactions" class="items-center flex flex-col gap-2">
|
<router-link to="/transactions" class="items-center flex flex-col gap-2">
|
||||||
<i class="pi pi-wallet text-2xl" style="font-size: 1.5rem"></i>
|
<i class="pi pi-wallet text-2xl" style="font-size: 1rem"></i>
|
||||||
<p>Транзакции</p>
|
<p>Транзакции</p>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class=" flex-col gap-2 p-2 hidden sm:flex">
|
|
||||||
|
<div class=" flex-col gap-2 p-2 flex">
|
||||||
<router-link to="/settings" class="items-center flex flex-col gap-2">
|
<router-link to="/settings" class="items-center flex flex-col gap-2">
|
||||||
<i class="pi pi-check text-2xl" style="font-size: 1.5rem"></i>
|
<i class="pi pi-cog text-2xl" style="font-size: 1rem"></i>
|
||||||
<p>Настройки</p>
|
<p>Настройки</p>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Создать с подменю -->
|
<!-- Создать с подменю -->
|
||||||
<div class="relative flex-col gap-2 p-2 items-center hidden sm:flex" @click="showSubmenu = !showSubmenu"
|
<div class="relative flex-col gap-2 p-2 items-center hidden sm:flex" @click="showSubmenu = !showSubmenu"
|
||||||
@mouseenter="showSubmenu = true" @mouseleave="showSubmenu = false">
|
@mouseenter="showSubmenu = true" @mouseleave="showSubmenu = false">
|
||||||
<!-- <router-link to="/transactions/create" class="items-center flex flex-col gap-2">-->
|
<!-- <router-link to="/transactions/create" class="items-center flex flex-col gap-2">-->
|
||||||
<i class="pi pi-cog text-2xl" style="font-size: 1.5rem"></i>
|
<i class="pi pi-cog text-2xl" style="font-size: 1rem"></i>
|
||||||
<p>Создать</p>
|
<p>Создать</p>
|
||||||
<!-- </router-link>-->
|
<!-- </router-link>-->
|
||||||
|
|
||||||
@@ -62,7 +71,7 @@
|
|||||||
<div class="relative flex-col gap-2 p-2 items-center flex sm:hidden" @click="showSubmenu = !showSubmenu"
|
<div class="relative flex-col gap-2 p-2 items-center flex sm:hidden" @click="showSubmenu = !showSubmenu"
|
||||||
@mouseenter="showSubmenu = true" @mouseleave="showSubmenu = false">
|
@mouseenter="showSubmenu = true" @mouseleave="showSubmenu = false">
|
||||||
<!-- <router-link to="/transactions/create" class="items-center flex flex-col gap-2">-->
|
<!-- <router-link to="/transactions/create" class="items-center flex flex-col gap-2">-->
|
||||||
<i class="pi pi-bars text-2xl" style="font-size: 1.5rem"></i>
|
<i class="pi pi-bars text-2xl" style="font-size: 1rem"></i>
|
||||||
<p>Меню</p>
|
<p>Меню</p>
|
||||||
<!-- </router-link>-->
|
<!-- </router-link>-->
|
||||||
|
|
||||||
@@ -102,6 +111,7 @@ import {useDrawerStore} from "@/stores/drawerStore";
|
|||||||
const showSubmenu = ref(false);
|
const showSubmenu = ref(false);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const transactionType = ref<TransactionType>()
|
const transactionType = ref<TransactionType>()
|
||||||
const categoryType = ref<CategoryType>()
|
const categoryType = ref<CategoryType>()
|
||||||
const drawerOpened = ref(false);
|
const drawerOpened = ref(false);
|
||||||
@@ -125,7 +135,7 @@ const openDrawer = (selectedTransactionType = null, selectedCategoryType = null)
|
|||||||
drawerStore.setCategoryType('EXPENSE')
|
drawerStore.setCategoryType('EXPENSE')
|
||||||
}
|
}
|
||||||
|
|
||||||
drawerStore.setVisible( true)
|
drawerStore.setVisible(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -157,7 +167,6 @@ const items = ref([
|
|||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// setTimeout(() => {
|
// setTimeout(() => {
|
||||||
// console.log(route.params['mode']);
|
// console.log(route.params['mode']);
|
||||||
|
|||||||
16
src/components/budgets/BudgetCreationDialogView.vue
Normal file
16
src/components/budgets/BudgetCreationDialogView.vue
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Dialog from "primevue/dialog";
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dialog :visible="opened" modal header="Создать новый бюджет" :style="{ width: '25rem' }" @hide="cancel" @update:visible="cancel">
|
||||||
|
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Dialog from "primevue/dialog";
|
|
||||||
import Checkbox from "primevue/checkbox";
|
import Checkbox from "primevue/checkbox";
|
||||||
import Button from "primevue/button";
|
import Button from "primevue/button";
|
||||||
import InputText from "primevue/inputtext";
|
import InputText from "primevue/inputtext";
|
||||||
@@ -10,6 +10,7 @@ import {getMonthName} from "@/utils/utils";
|
|||||||
import {Budget} from "@/models/Budget";
|
import {Budget} from "@/models/Budget";
|
||||||
import {getCategories} from "@/services/categoryService";
|
import {getCategories} from "@/services/categoryService";
|
||||||
import {useSpaceStore} from "@/stores/spaceStore";
|
import {useSpaceStore} from "@/stores/spaceStore";
|
||||||
|
import {createBudget, getBudgetInfos} from "@/services/budgetsService";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
opened: {
|
opened: {
|
||||||
@@ -17,8 +18,8 @@ const props = defineProps({
|
|||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const emits = defineEmits(['budget-created','close-modal'])
|
const emits = defineEmits(['budget-created', 'close-modal'])
|
||||||
const createRecurrentPayments = ref<Boolean>(true)
|
const createRecurrentPayments = ref<Boolean>(false)
|
||||||
|
|
||||||
const name = ref('')
|
const name = ref('')
|
||||||
const dateFrom = ref(new Date())
|
const dateFrom = ref(new Date())
|
||||||
@@ -29,6 +30,10 @@ const budget = ref(new Budget())
|
|||||||
const create = async () => {
|
const create = async () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
await createBudget(budget.value, createRecurrentPayments)
|
||||||
|
.then((res) => {
|
||||||
|
budget.value = res
|
||||||
|
})
|
||||||
emits("budget-created", budget.value, createRecurrentPayments.value);
|
emits("budget-created", budget.value, createRecurrentPayments.value);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
@@ -36,6 +41,22 @@ const create = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
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 categories = ref([])
|
const categories = ref([])
|
||||||
const fetchCategories = async () => {
|
const fetchCategories = async () => {
|
||||||
await getCategories().then(res => categories.value = res.data)
|
await getCategories().then(res => categories.value = res.data)
|
||||||
@@ -90,11 +111,9 @@ onMounted(() => {
|
|||||||
fetchCategories()
|
fetchCategories()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Dialog :visible="opened" modal header="Создать новый бюджет" :style="{ width: '25rem' }" @hide="cancel" @update:visible="cancel">
|
|
||||||
<div class="flex flex-col gap-4 mt-1">
|
<div class="flex flex-col gap-4 mt-1">
|
||||||
<FloatLabel variant="on" class="w-full">
|
<FloatLabel variant="on" class="w-full">
|
||||||
<label for="name">Название</label>
|
<label for="name">Название</label>
|
||||||
@@ -110,18 +129,16 @@ onMounted(() => {
|
|||||||
<DatePicker v-model="budget.dateTo" id="dateTo" dateFormat="dd.mm.yy"/>
|
<DatePicker v-model="budget.dateTo" id="dateTo" dateFormat="dd.mm.yy"/>
|
||||||
</FloatLabel>
|
</FloatLabel>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row items-center min-w-fit gap-4">
|
<!-- <div class="flex flex-row items-center min-w-fit gap-4">-->
|
||||||
<Checkbox v-model="createRecurrentPayments" binary/>
|
<!-- <Checkbox v-model="createRecurrentPayments" binary/>-->
|
||||||
Создать ежемесячные платежи?
|
<!-- Создать ежемесячные платежи?-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
<div v-if="categories.length ==0" class="text-red-500 font-bold">Сперва лучше создать категории</div>
|
<div v-if="categories.length ==0" class="text-red-500 font-bold">Сперва лучше создать категории</div>
|
||||||
<div class="flex flex-row gap-2 justify-end items-center">
|
<div class="flex flex-row gap-2 justify-end items-center">
|
||||||
<Button label="Создать" severity="success" icon="pi pi-save" @click="create"/>
|
<Button label="Создать" severity="success" icon="pi pi-save" @click="create"/>
|
||||||
<Button label="Отмена" severity="secondary" icon="pi pi-times-circle" @click="cancel"/>
|
<Button label="Отмена" severity="secondary" icon="pi pi-times-circle" @click="cancel"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div class="flex flex-row gap-4 items-center">
|
<div class="flex flex-row gap-4 items-center">
|
||||||
<h2 class="text-4xl font-bold">Бюджеты</h2>
|
<h2 class="text-4xl font-bold">Бюджеты</h2>
|
||||||
<Button label="+ Создать" @click="creationOpened=true" size="small"/>
|
<Button label="+ Создать" @click="creationOpened=true" size="small"/>
|
||||||
<BudgetCreationView :opened="creationOpened" @budget-created="creationSuccessShow"
|
<BudgetCreationDialogView :opened="creationOpened" @budget-created="creationSuccessShow"
|
||||||
@close-modal="creationOpened=false"/>
|
@close-modal="creationOpened=false"/>
|
||||||
<StatusView :show="creationSuccessModal" :is-error="false" :message="'Бюджет создан!'"/>
|
<StatusView :show="creationSuccessModal" :is-error="false" :message="'Бюджет создан!'"/>
|
||||||
</div>
|
</div>
|
||||||
@@ -58,7 +58,7 @@ import {formatDate} from "@/utils/utils";
|
|||||||
import LoadingView from "@/components/LoadingView.vue";
|
import LoadingView from "@/components/LoadingView.vue";
|
||||||
import Button from "primevue/button";
|
import Button from "primevue/button";
|
||||||
import ConfirmDialog from "primevue/confirmdialog";
|
import ConfirmDialog from "primevue/confirmdialog";
|
||||||
import BudgetCreationView from "@/components/budgets/BudgetCreationView.vue";
|
import BudgetCreationDialogView from "@/components/budgets/BudgetCreationDialogView.vue";
|
||||||
import StatusView from "@/components/StatusView.vue";
|
import StatusView from "@/components/StatusView.vue";
|
||||||
import {useConfirm} from "primevue/useconfirm";
|
import {useConfirm} from "primevue/useconfirm";
|
||||||
import {useToast} from "primevue/usetoast";
|
import {useToast} from "primevue/usetoast";
|
||||||
@@ -73,22 +73,7 @@ const budgetInfos = ref<BudgetInfo[]>([])
|
|||||||
const creationOpened = ref(false)
|
const creationOpened = ref(false)
|
||||||
const creationSuccessModal = ref(false)
|
const creationSuccessModal = ref(false)
|
||||||
|
|
||||||
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 deleteBudget = async (budget: Budget) => {
|
const deleteBudget = async (budget: Budget) => {
|
||||||
|
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ onMounted(async () => {
|
|||||||
<p :class="transaction.isDone && isPlanned && !props.isList ? 'line-through' : ''" class="font-bold">{{
|
<p :class="transaction.isDone && isPlanned && !props.isList ? 'line-through' : ''" class="font-bold">{{
|
||||||
transaction.comment
|
transaction.comment
|
||||||
}}</p>
|
}}</p>
|
||||||
<p :class="transaction.isDone && isPlanned && !props.isList ? 'line-through' : ''" class="font-light">
|
<p :class="transaction.isDone && isPlanned && !props.isList ? 'line-through' : ''" class="font-light text-start">
|
||||||
{{ isPlanned ? transaction.category.icon : '' }} {{
|
{{ isPlanned ? transaction.category.icon : '' }} {{
|
||||||
transaction.category.name
|
transaction.category.name
|
||||||
}} |
|
}} |
|
||||||
|
|||||||
@@ -92,7 +92,14 @@ const totalIncomeLeftToGet = computed(() => {
|
|||||||
|
|
||||||
const totalLoans = computed(() => {
|
const totalLoans = computed(() => {
|
||||||
let value = 0
|
let value = 0
|
||||||
categories.value.filter((cat) => cat.category.id == "677bc767c7857460a491bd49").forEach(cat => {
|
|
||||||
|
categories.value.filter((cat) =>
|
||||||
|
cat.category.tags.some(tag => {
|
||||||
|
|
||||||
|
return tag.code == "loans";
|
||||||
|
})
|
||||||
|
).forEach(cat => {
|
||||||
|
// console.log(cat)
|
||||||
value += cat.currentLimit
|
value += cat.currentLimit
|
||||||
})
|
})
|
||||||
return value
|
return value
|
||||||
@@ -258,13 +265,14 @@ watch(
|
|||||||
|
|
||||||
// 4️⃣ Computed для группировки транзакций расходов по категориям
|
// 4️⃣ Computed для группировки транзакций расходов по категориям
|
||||||
const categoriesTransactions = computed(() => {
|
const categoriesTransactions = computed(() => {
|
||||||
const grouped: Record<string, { name: any; transactions: any[]; isOpened: boolean }> = {};
|
const grouped: Record<string, { name: any; transactions: any[]; notDoneValue: any; isOpened: boolean }> = {};
|
||||||
|
|
||||||
// Создаем группу для каждой категории из списка категорий расходов (categories)
|
// Создаем группу для каждой категории из списка категорий расходов (categories)
|
||||||
categories.value.forEach((category) => {
|
categories.value.forEach((category) => {
|
||||||
grouped[category.category.id] = {
|
grouped[category.category.id] = {
|
||||||
name: category,
|
name: category,
|
||||||
transactions: [],
|
transactions: [],
|
||||||
|
notDoneValue: 0,
|
||||||
isOpened: expenseCategoriesState[category.category.id]?.isOpened ?? true,
|
isOpened: expenseCategoriesState[category.category.id]?.isOpened ?? true,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -272,14 +280,21 @@ const categoriesTransactions = computed(() => {
|
|||||||
// Добавляем транзакции расходов (plannedExpenses)
|
// Добавляем транзакции расходов (plannedExpenses)
|
||||||
plannedExpenses.value.forEach((plannedExpense) => {
|
plannedExpenses.value.forEach((plannedExpense) => {
|
||||||
const categoryId = plannedExpense.category.id;
|
const categoryId = plannedExpense.category.id;
|
||||||
|
// console.log(!plannedExpense.isDone ? grouped[categoryId].notDoneValue + plannedExpense.amount : grouped[categoryId].notDoneValue + 0 )
|
||||||
|
|
||||||
if (!grouped[categoryId]) {
|
if (!grouped[categoryId]) {
|
||||||
grouped[categoryId] = {
|
grouped[categoryId] = {
|
||||||
name: plannedExpense.category,
|
name: plannedExpense.category,
|
||||||
transactions: [],
|
transactions: [],
|
||||||
|
notDoneValue: !plannedExpense.isDone ? grouped[categoryId].notDoneValue + plannedExpense.amount : grouped[categoryId].notDoneValue + 0,
|
||||||
isOpened: expenseCategoriesState[categoryId]?.isOpened ?? true,
|
isOpened: expenseCategoriesState[categoryId]?.isOpened ?? true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
grouped[categoryId].transactions.push(plannedExpense);
|
grouped[categoryId].transactions.push(plannedExpense);
|
||||||
|
if (!plannedExpense.isDone) {
|
||||||
|
grouped[categoryId].notDoneValue += plannedExpense.amount;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Преобразуем объект в массив, сортируем, затем превращаем обратно в объект
|
// Преобразуем объект в массив, сортируем, затем превращаем обратно в объект
|
||||||
@@ -728,7 +743,11 @@ watch([budget, plannedExpenses], () => {
|
|||||||
|
|
||||||
calendar.value = result;
|
calendar.value = result;
|
||||||
}, {immediate: true});
|
}, {immediate: true});
|
||||||
|
const mobileViewModes = ref([{code: "planned", name: "Планирование"}, {
|
||||||
|
code: "transactions",
|
||||||
|
name: "Фактические траты"
|
||||||
|
}])
|
||||||
|
const mobileViewMode = ref({code: "planned", name: "Планирование"})
|
||||||
const spaceStore = useSpaceStore()
|
const spaceStore = useSpaceStore()
|
||||||
const selectedSpace = computed(() => spaceStore.space)
|
const selectedSpace = computed(() => spaceStore.space)
|
||||||
|
|
||||||
@@ -822,7 +841,7 @@ onUnmounted(async () => {
|
|||||||
<h3 class="text-2xl font-bold text-gray-700">Аналитика</h3>
|
<h3 class="text-2xl font-bold text-gray-700">Аналитика</h3>
|
||||||
<div class=" flex flex-col gap-3 items-start ">
|
<div class=" flex flex-col gap-3 items-start ">
|
||||||
<div class=" bg-white flex p-4 flex-col shadow-md rounded-lg w-full h-full">
|
<div class=" bg-white flex p-4 flex-col shadow-md rounded-lg w-full h-full">
|
||||||
<SelectButton v-model="selectedChart" :options="modes" optionLabel="label" optionIcon="icon">
|
<SelectButton v-model="selectedChart" :options="modes" optionLabel="label" optionIcon="icon" style=" border-width: 0px !important; ">
|
||||||
<template #option="slotProps">
|
<template #option="slotProps">
|
||||||
<i :class="slotProps.option.icon"></i>
|
<i :class="slotProps.option.icon"></i>
|
||||||
</template>
|
</template>
|
||||||
@@ -831,20 +850,22 @@ onUnmounted(async () => {
|
|||||||
:options="incomeExpenseChartOptions" class="!w-full"
|
:options="incomeExpenseChartOptions" class="!w-full"
|
||||||
style="width: 100%"/>
|
style="width: 100%"/>
|
||||||
|
|
||||||
<Chart v-if="selectedChart.value=='pie'" type="pie" :data="pieChartData" :options="pieChartOptions"
|
<div v-if="selectedChart.value=='pie'" class="chart w-full flex items-center justify-center">
|
||||||
class="chart "/>
|
<Chart type="pie" :data="pieChartData" :options="pieChartOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div></div>
|
<div></div>
|
||||||
<div class=" flex flex-col gap-2 relative border-[0.1rem] rounded-xl p-2 w-full h-full pt-4">
|
<div class=" flex flex-col gap-2 relative border-[0.1rem] rounded-xl p-2 w-full h-full pt-4">
|
||||||
<p class="absolute -top-3 left-1/2 -translate-x-1/2 bg-gray-100 px-2 text-gray-700 font-semibold">
|
<p class="absolute -top-3 left-1/2 -translate-x-1/2 bg-gray-100 px-2 text-gray-700 font-semibold">
|
||||||
Запланировано</p>
|
Запланировано</p>
|
||||||
<div class="flex gap-5 items-center justify-items-center w-full ">
|
<div class="flex gap-5 items-center justify-items-center w-full ">
|
||||||
<button class="flex flex-row gap-1 items-center w-full" @click="detailedShowed = !detailedShowed">
|
<div class="flex flex-row gap-1 items-center w-full">
|
||||||
<div
|
<div
|
||||||
class="flex flex-col items-center font-bold bg-white p-2 shadow-md rounded-lg w-full h-full ">
|
class="flex flex-col items-center font-bold bg-white p-2 shadow-md rounded-lg w-full h-full ">
|
||||||
<div class="group justify-end flex w-full">
|
<div class="group flex flex-row justify-between w-full">
|
||||||
<i class="pi pi-info-circle w-full text-end" style="font-size: 0.7rem"
|
<i class="pi pi-info-circle w-full text-end" style="font-size: 0.7rem"
|
||||||
v-tooltip="'Рассчитывается по принципу сумма плановых поступлений'"/>
|
v-tooltip.focus="'Рассчитывается по принципу сумма плановых поступлений'" tabindex="1"/>
|
||||||
<!-- <div class="absolute w-fit bg-gray-800 text-white text-xs px-2 py-1 rounded hidden group-hover:block transition-opacity cursor-pointer">-->
|
<!-- <div class="absolute w-fit bg-gray-800 text-white text-xs px-2 py-1 rounded hidden group-hover:block transition-opacity cursor-pointer">-->
|
||||||
<!-- Рассчитывается по принципу сумма плановых поступлений-->
|
<!-- Рассчитывается по принципу сумма плановых поступлений-->
|
||||||
<!-- </div>-->
|
<!-- </div>-->
|
||||||
@@ -861,7 +882,8 @@ onUnmounted(async () => {
|
|||||||
<div class="flex flex-col items-center bg-white p-2 shadow-md rounded-lg w-full h-full ">
|
<div class="flex flex-col items-center bg-white p-2 shadow-md rounded-lg w-full h-full ">
|
||||||
<div class="group justify-end flex w-full">
|
<div class="group justify-end flex w-full">
|
||||||
<i class="pi pi-info-circle w-full text-end" style="font-size: 0.7rem"
|
<i class="pi pi-info-circle w-full text-end" style="font-size: 0.7rem"
|
||||||
v-tooltip="'Рассчитывается по принципу сумма плановых трат + разница между суммой плана категории и лимитом категории'"/>
|
v-tooltip.focus="'Рассчитывается по принципу сумма плановых трат + разница между суммой плана категории и лимитом категории'"
|
||||||
|
tabindex="1"/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center">
|
||||||
@@ -878,7 +900,7 @@ onUnmounted(async () => {
|
|||||||
class="flex flex-col items-center font-bold bg-white p-2 shadow-md rounded-lg w-full h-full ">
|
class="flex flex-col items-center font-bold bg-white p-2 shadow-md rounded-lg w-full h-full ">
|
||||||
<div class="group justify-end flex w-full">
|
<div class="group justify-end flex w-full">
|
||||||
<i class="pi pi-info-circle w-full text-end" style="font-size: 0.7rem"
|
<i class="pi pi-info-circle w-full text-end" style="font-size: 0.7rem"
|
||||||
v-tooltip="'Разница между плановыми приходами и расходами'"/>
|
v-tooltip.focus="'Разница между плановыми приходами и расходами'" tabindex="1"/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<h4 class="text-lg font-bold">Остаток</h4>
|
<h4 class="text-lg font-bold">Остаток</h4>
|
||||||
@@ -891,7 +913,7 @@ onUnmounted(async () => {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</button>
|
</div>
|
||||||
|
|
||||||
<!-- <div class="grid grid-cols-2 !gap-1 mt-4" :class="detailedShowed ? 'block' : 'hidden'">-->
|
<!-- <div class="grid grid-cols-2 !gap-1 mt-4" :class="detailedShowed ? 'block' : 'hidden'">-->
|
||||||
<!-- <div class="flex flex-col items-center font-bold ">-->
|
<!-- <div class="flex flex-col items-center font-bold ">-->
|
||||||
@@ -926,13 +948,14 @@ onUnmounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="grid gap-5 items-center justify-items-center w-full">
|
<div class="grid gap-5 items-center justify-items-center w-full">
|
||||||
<div class="w-full ">
|
<div class="w-full ">
|
||||||
<button class="flex flex-row justify-between gap-2 items-end w-full"
|
<div class="flex flex-row justify-between gap-2 items-end w-full"
|
||||||
@click="detailedShowed = !detailedShowed">
|
>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col items-center font-bold bg-white p-2 shadow-md rounded-lg w-full h-full">
|
class="flex flex-col items-center font-bold bg-white p-2 shadow-md rounded-lg w-full h-full">
|
||||||
<div class="group justify-end flex w-full">
|
<div class="group justify-end flex w-full">
|
||||||
<i class="pi pi-info-circle w-full text-end" style="font-size: 0.7rem"
|
<i class="pi pi-info-circle w-full text-end" style="font-size: 0.7rem"
|
||||||
v-tooltip="'Сумма категории Кредиты и долги в сумме лимитов категорий'"/>
|
v-tooltip.focus="'Для корректного подсчета нужно выставить тэг loans на категориях долгов'"
|
||||||
|
tabindex="1"/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<h4 class="text-sm lg:text-base">Долги</h4>
|
<h4 class="text-sm lg:text-base">Долги</h4>
|
||||||
@@ -947,7 +970,8 @@ onUnmounted(async () => {
|
|||||||
class="flex flex-col items-center font-bold bg-white p-2 shadow-md rounded-lg w-full h-full">
|
class="flex flex-col items-center font-bold bg-white p-2 shadow-md rounded-lg w-full h-full">
|
||||||
<div class="group justify-end flex w-full">
|
<div class="group justify-end flex w-full">
|
||||||
<i class="pi pi-info-circle w-full text-end" style="font-size: 0.7rem"
|
<i class="pi pi-info-circle w-full text-end" style="font-size: 0.7rem"
|
||||||
v-tooltip="'Для корректного подсчета нужно выставить тэг savings на категориях сбережений'"/>
|
v-tooltip.focus="'Для корректного подсчета нужно выставить тэг savings на категориях сбережений'"
|
||||||
|
tabindex="1"/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<span class="text-sm lg:text-base font-bold">Сбережения</span>
|
<span class="text-sm lg:text-base font-bold">Сбережения</span>
|
||||||
@@ -963,7 +987,7 @@ onUnmounted(async () => {
|
|||||||
class="flex flex-col items-center font-bold bg-white p-2 shadow-md rounded-lg w-full h-full">
|
class="flex flex-col items-center font-bold bg-white p-2 shadow-md rounded-lg w-full h-full">
|
||||||
<div class="group justify-end flex w-full">
|
<div class="group justify-end flex w-full">
|
||||||
<i class="pi pi-info-circle w-full text-end" style="font-size: 0.7rem"
|
<i class="pi pi-info-circle w-full text-end" style="font-size: 0.7rem"
|
||||||
v-tooltip="'Сумма остатка лимитов по остальным категориям'"/>
|
v-tooltip.focus="'Сумма остатка лимитов по остальным категориям'" tabindex="1"/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<h4 class="text-sm lg:text-base">Ежедневные</h4>
|
<h4 class="text-sm lg:text-base">Ежедневные</h4>
|
||||||
@@ -973,7 +997,7 @@ onUnmounted(async () => {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -984,8 +1008,8 @@ onUnmounted(async () => {
|
|||||||
<p class="absolute -top-3 left-1/2 -translate-x-1/2 bg-gray-100 px-2 text-gray-700 font-semibold">
|
<p class="absolute -top-3 left-1/2 -translate-x-1/2 bg-gray-100 px-2 text-gray-700 font-semibold">
|
||||||
Фактические</p>
|
Фактические</p>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<button class="flex flex-row justify-between gap-1 items-center w-full"
|
<div class="flex flex-row justify-between gap-1 items-center w-full"
|
||||||
@click="detailedShowed = !detailedShowed">
|
>
|
||||||
<div class="flex flex-col items-center font-bold bg-white p-2 shadow-md rounded-lg w-full h-full">
|
<div class="flex flex-col items-center font-bold bg-white p-2 shadow-md rounded-lg w-full h-full">
|
||||||
<h4 class="text-sm lg:text-base">Приходы</h4>
|
<h4 class="text-sm lg:text-base">Приходы</h4>
|
||||||
<div class="font-light text-center w-full ">
|
<div class="font-light text-center w-full ">
|
||||||
@@ -1010,7 +1034,7 @@ onUnmounted(async () => {
|
|||||||
{{ formatAmount(totalInstantIncomes - totalInstantExpenses) }} ₽
|
{{ formatAmount(totalInstantIncomes - totalInstantExpenses) }} ₽
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 !gap-2 mt-4" :class="detailedShowed ? 'block' : 'hidden'">
|
<div class="grid grid-cols-2 !gap-2 mt-4" :class="detailedShowed ? 'block' : 'hidden'">
|
||||||
<div v-for="categorySum in transactionCategoriesSums"
|
<div v-for="categorySum in transactionCategoriesSums"
|
||||||
@@ -1054,11 +1078,15 @@ onUnmounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="grid grid-cols-1 gap-1 row-span-3 col-span-2">
|
||||||
|
<div class="w-full flex items-center justify-center px-2 py-2 bg-gray-100 md:hidden flex">
|
||||||
|
<SelectButton v-model="mobileViewMode" :options="mobileViewModes" option-label="name"
|
||||||
|
class=" "/>
|
||||||
|
</div>
|
||||||
|
<div v-if="mobileViewMode.code=='planned'" class="flex flex-col gap-4">
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-4 row-span-3 col-span-2">
|
|
||||||
<div>
|
|
||||||
<!-- Планируемые доходы -->
|
<!-- Планируемые доходы -->
|
||||||
<div>
|
<div >
|
||||||
<div class="flex flex-row gap-4 items-center mb-4">
|
<div class="flex flex-row gap-4 items-center mb-4">
|
||||||
<h3 class="text-2xl font-bold text-emerald-500 ">Поступления</h3>
|
<h3 class="text-2xl font-bold text-emerald-500 ">Поступления</h3>
|
||||||
<button @click="openDrawer('PLANNED', 'INCOME')">
|
<button @click="openDrawer('PLANNED', 'INCOME')">
|
||||||
@@ -1117,18 +1145,18 @@ onUnmounted(async () => {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<!-- Планируемые расходы -->
|
<!-- Планируемые расходы -->
|
||||||
<div class="pb-4">
|
<div class="flex flex-col gap-1">
|
||||||
|
|
||||||
<div class="flex flex-row gap-4 items-center mb-4">
|
<div class="flex flex-row gap-0 items-center ">
|
||||||
<h3 class="text-2xl font-bold text-rose-500 ">Расходы</h3>
|
<h3 class="text-2xl font-bold text-rose-500 ">Расходы</h3>
|
||||||
<button @click="openDrawer('PLANNED', 'EXPENSE')">
|
<button @click="openDrawer('PLANNED', 'EXPENSE')">
|
||||||
<span class="font-light text-sm">+ Добавить</span>
|
<span class="font-light text-sm">+ Добавить</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-4 pb-4">
|
<div class="flex flex-col gap-1 ">
|
||||||
<div class="flex flex-row gap-2"><span class="text-lg font-bold items-center">Календарь</span>
|
<div class="flex flex-row gap-2"><span class="text-lg font-bold items-center">Календарь</span>
|
||||||
<button class="font-light text-sm" @click="toggleCalendar">
|
<button class="font-light text-sm" @click="toggleCalendar">
|
||||||
{{ calendarExpanded ? 'Скрыть' : 'Раскрыть' }}
|
{{ calendarExpanded ? 'Скрыть' : 'Раскрыть' }}
|
||||||
@@ -1154,7 +1182,7 @@ onUnmounted(async () => {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<ul class="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||||
<li v-for="(category, categoryId) in categoriesTransactions" :key="categoryId"
|
<li v-for="(category, categoryId) in categoriesTransactions" :key="categoryId"
|
||||||
class="flex flex-col justify-between p-4 shadow-md rounded-lg bg-white ">
|
class="flex flex-col justify-between p-4 shadow-md rounded-lg bg-white ">
|
||||||
<div class="">
|
<div class="">
|
||||||
@@ -1210,36 +1238,48 @@ onUnmounted(async () => {
|
|||||||
@transaction-updated="updateTransactions"
|
@transaction-updated="updateTransactions"
|
||||||
@transaction-checked="updateTransactions"
|
@transaction-checked="updateTransactions"
|
||||||
/>
|
/>
|
||||||
<div
|
<!-- <div-->
|
||||||
class="flex justify-between bg-white min-w-fit max-h-fit flex-row items-center gap-4 w-full ">
|
<!-- class="flex justify-between bg-white min-w-fit max-h-fit flex-row items-center gap-4 w-full ">-->
|
||||||
<div>
|
<!-- <div>-->
|
||||||
<p class=" font-bold text-gray-700 dark:text-gray-400">
|
<!-- <p class=" font-bold text-gray-700 dark:text-gray-400">-->
|
||||||
🗓️ Остаток на внеплановые
|
<!-- 🗓️ Остаток на внеплановые-->
|
||||||
</p>
|
<!-- </p>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
<div class="text-lg line-clamp-1 ">
|
<!-- <div class="text-lg line-clamp-1 ">-->
|
||||||
{{
|
<!-- {{-->
|
||||||
formatAmount(category.name.currentLimit - category.name.currentPlanned)
|
<!-- formatAmount(category.name.currentLimit - category.name.currentPlanned)-->
|
||||||
}} ₽
|
<!-- }} ₽-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-0">
|
<div class="flex flex-col gap-0">
|
||||||
<div class="flex flex-row justify-between w-full items-center">
|
<div class="flex flex-row gap-1 w-full items-center">
|
||||||
<span class="font-bold">🗓️ Остаток всего:</span>
|
|
||||||
<span class="font-bold text-lg">{{
|
<span class="font-bold">{{
|
||||||
formatAmount(category.name.currentLimit - category.name.currentSpent)
|
category.name.currentLimit - category.name.currentSpent >= 0 ? 'Остаток в категории' : 'Превышение на'
|
||||||
|
}}</span>
|
||||||
|
<span class="font-bold "
|
||||||
|
:class="category.name.currentLimit - category.name.currentSpent < 0? 'text-red-500' : ''">{{
|
||||||
|
formatAmount(Math.abs(category.name.currentLimit - category.name.currentSpent))
|
||||||
}} ₽</span>
|
}} ₽</span>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<span v-if="category.transactions.filter(t => !t.isDone).length>0" class="font-light "
|
||||||
|
:class="category.name.currentLimit - category.name.currentSpent - category.notDoneValue < 0 ? 'text-red-500': ''">искл. оставшиеся плановые
|
||||||
|
<!-- {{ category.transactions.filter(t => !t.isDone ).forEach( t=> t.amount)}}-->
|
||||||
|
{{
|
||||||
|
formatAmount(category.name.currentLimit - category.name.currentSpent - category.notDoneValue)
|
||||||
|
}} ₽</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class=" h-fit gap-4 flex-col row-span-6 lg:hidden ">
|
|
||||||
|
<div v-else class=" h-fit gap-4 flex-col row-span-6 lg:hidden ">
|
||||||
<div class="flex flex-row ">
|
<div class="flex flex-row ">
|
||||||
<h3 class="text-2xl font-bold">Транзакции</h3>
|
<h3 class="text-2xl font-bold">Транзакции</h3>
|
||||||
<button @click="openDrawer('INSTANT', 'EXPENSE')">
|
<button @click="openDrawer('INSTANT', 'EXPENSE')">
|
||||||
@@ -1267,6 +1307,7 @@ onUnmounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -1300,8 +1341,7 @@ onUnmounted(async () => {
|
|||||||
box-shadow: inset 0 2px 4px 0 rgb(0 0 0 / 0.3);
|
box-shadow: inset 0 2px 4px 0 rgb(0 0 0 / 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart {
|
|
||||||
width: 70%;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@@ -36,7 +36,7 @@ onMounted(async () => {
|
|||||||
<div v-else class="">
|
<div v-else class="">
|
||||||
<div class="flex flex-col bg-gray-200 outline outline-2 outline-gray-300 rounded-2xl p-4">
|
<div class="flex flex-col bg-gray-200 outline outline-2 outline-gray-300 rounded-2xl p-4">
|
||||||
<div class="flex flex-row items-center min-w-fit justify-between">
|
<div class="flex flex-row items-center min-w-fit justify-between">
|
||||||
<p class="text-2xl font-bold">Категории</p>
|
<p class="text-2xl ">Категории</p>
|
||||||
<router-link to="/settings/categories">
|
<router-link to="/settings/categories">
|
||||||
<Button size="large" icon="pi pi-arrow-circle-right" severity="secondary" text rounded/>
|
<Button size="large" icon="pi pi-arrow-circle-right" severity="secondary" text rounded/>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|||||||
@@ -101,38 +101,38 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-5 pb-5">
|
<div class="flex flex-col gap-5 pb-5">
|
||||||
<h1 class="font-bold text-2xl leading-tight">Общие настройки</h1>
|
<h1 class=" text-4xl leading-tight">Общие настройки</h1>
|
||||||
<div class="flex flex-col gap-2">
|
<!-- <div class="flex flex-col gap-2">-->
|
||||||
<h2 class="font-bold text-xl leading-tight">Настройки периодов</h2>
|
<!-- <h2 class="font-bold text-xl leading-tight">Настройки периодов</h2>-->
|
||||||
<form class="flex flex-col gap-4">
|
<!-- <form class="flex flex-col gap-4">-->
|
||||||
<div class="flex flex-row items-center gap-2 ">
|
<!-- <div class="flex flex-row items-center gap-2 ">-->
|
||||||
<label for="username">День первого поступления средств</label>
|
<!-- <label for="username">День первого поступления средств</label>-->
|
||||||
<InputNumber id="username" v-model="firstDay" class="w-10" inputId="horizontal-buttons" showButtons
|
<!-- <InputNumber id="username" v-model="firstDay" class="w-10" inputId="horizontal-buttons" showButtons-->
|
||||||
buttonLayout="horizontal" :min="1" :max="31">
|
<!-- buttonLayout="horizontal" :min="1" :max="31">-->
|
||||||
<template #incrementbuttonicon>
|
<!-- <template #incrementbuttonicon>-->
|
||||||
<span class="pi pi-plus"/>
|
<!-- <span class="pi pi-plus"/>-->
|
||||||
</template>
|
<!-- </template>-->
|
||||||
<template #decrementbuttonicon>
|
<!-- <template #decrementbuttonicon>-->
|
||||||
<span class="pi pi-minus"/>
|
<!-- <span class="pi pi-minus"/>-->
|
||||||
</template>
|
<!-- </template>-->
|
||||||
</InputNumber>
|
<!-- </InputNumber>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
<div class="flex flex-row items-center gap-2">
|
<!-- <div class="flex flex-row items-center gap-2">-->
|
||||||
<label for="username">День второго поступления средств</label>
|
<!-- <label for="username">День второго поступления средств</label>-->
|
||||||
<InputNumber id="username" v-model="secondDay" inputId="horizontal-buttons" showButtons
|
<!-- <InputNumber id="username" v-model="secondDay" inputId="horizontal-buttons" showButtons-->
|
||||||
buttonLayout="horizontal" :min="1" :max="31">
|
<!-- buttonLayout="horizontal" :min="1" :max="31">-->
|
||||||
<template #incrementbuttonicon>
|
<!-- <template #incrementbuttonicon>-->
|
||||||
<span class="pi pi-plus"/>
|
<!-- <span class="pi pi-plus"/>-->
|
||||||
</template>
|
<!-- </template>-->
|
||||||
<template #decrementbuttonicon>
|
<!-- <template #decrementbuttonicon>-->
|
||||||
<span class="pi pi-minus"/>
|
<!-- <span class="pi pi-minus"/>-->
|
||||||
</template>
|
<!-- </template>-->
|
||||||
</InputNumber>
|
<!-- </InputNumber>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
|
|
||||||
|
|
||||||
</form>
|
<!-- </form>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
<div class="flex flex-col gap-2 justify-start">
|
<div class="flex flex-col gap-2 justify-start">
|
||||||
<h2 class="font-bold text-xl leading-tight">Тэги категорий</h2>
|
<h2 class="font-bold text-xl leading-tight">Тэги категорий</h2>
|
||||||
<div class="flex flex-row items-center gap-2 flex-wrap">
|
<div class="flex flex-row items-center gap-2 flex-wrap">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import CategorySettingView from "@/components/settings/CategorySettingView.vue";
|
import CategorySettingView from "@/components/settings/CategorySettingView.vue";
|
||||||
import RecurrentSettingView from "@/components/settings/RecurrentSettingView.vue";
|
import RecurrentSettingView from "@/components/settings/RecurrentSettingView.vue";
|
||||||
|
|
||||||
import {ref} from "vue";
|
import {onMounted, ref} from "vue";
|
||||||
import Divider from "primevue/divider";
|
import Divider from "primevue/divider";
|
||||||
import CategoriesList from "@/components/settings/categories/CategoriesList.vue";
|
import CategoriesList from "@/components/settings/categories/CategoriesList.vue";
|
||||||
import RecurrentList from "@/components/settings/recurrent/RecurrentList.vue";
|
import RecurrentList from "@/components/settings/recurrent/RecurrentList.vue";
|
||||||
@@ -11,6 +11,18 @@ import CommonSettings from "@/components/settings/CommonSettings.vue";
|
|||||||
|
|
||||||
const selectedModeCode = ref("common")
|
const selectedModeCode = ref("common")
|
||||||
|
|
||||||
|
const selectMode = (mode: string) => {
|
||||||
|
localStorage.setItem("selectedSettingPage", mode);
|
||||||
|
selectedModeCode.value = mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const localStorageMode = localStorage.getItem("selectedSettingPage");
|
||||||
|
if (localStorageMode) {
|
||||||
|
selectedModeCode.value = localStorageMode;
|
||||||
|
} else selectedModeCode.value = "common";
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
const pages = ref([
|
const pages = ref([
|
||||||
{
|
{
|
||||||
@@ -46,26 +58,29 @@ const pages = ref([
|
|||||||
<ul class=" flex-col gap-1 ">
|
<ul class=" flex-col gap-1 ">
|
||||||
<li v-for="page in pages" :key="page.code"
|
<li v-for="page in pages" :key="page.code"
|
||||||
class="flex flex-row gap-2 p-2 hover:bg-emerald-100 hover:text-emerald-700 hover:rounded-md items-center"
|
class="flex flex-row gap-2 p-2 hover:bg-emerald-100 hover:text-emerald-700 hover:rounded-md items-center"
|
||||||
:class="selectedModeCode == page.code ? 'bg-blue-100' : ''" @click="selectedModeCode = page.code">
|
:class="selectedModeCode == page.code ? 'bg-blue-100' : ''" @click="selectMode(page.code)">
|
||||||
<i :class="page.icon"/>{{ page.title }}
|
<i :class="page.icon"/>{{ page.title }}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<Divider layout="vertical"/>
|
<Divider layout="vertical"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex xl:hidden sm:col-span-1 w-full h-fit p-2 mb-2 overflow-y-scroll outline outline-1 outline-gray-300 rounded-xl">
|
<div
|
||||||
|
class="flex xl:hidden sm:col-span-1 w-full h-fit p-2 mb-2 overflow-y-scroll outline outline-1 outline-gray-300 rounded-xl">
|
||||||
<ul class=" min-w-fit w-full items-start">
|
<ul class=" min-w-fit w-full items-start">
|
||||||
<li v-for="page in pages"
|
<li v-for="page in pages"
|
||||||
class=" flex flex-row gap-2 px-2 py-2 hover:bg-blue-50 rounded-md line-clamp-1 w-full"
|
class=" flex flex-row gap-2 px-2 py-2 hover:bg-blue-50 rounded-md line-clamp-1 w-full"
|
||||||
:class="selectedModeCode == page.code ? '!bg-emerald-50 text-emerald-700': '' " @click="selectedModeCode=page.code">
|
:class="selectedModeCode == page.code ? '!bg-emerald-50 text-emerald-700': '' "
|
||||||
|
@click="selectMode(page.code)">
|
||||||
<i :class="page.icon"/>{{ page.title }}
|
<i :class="page.icon"/>{{ page.title }}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class=" flex flex-col col-span-5 gap-2 max-h-[90dvh] overflow-y-scroll justify-items-start">
|
<div class=" flex flex-col col-span-5 gap-2 justify-start justify-items-start">
|
||||||
<CommonSettings v-if="selectedModeCode == 'common'"/>
|
<CommonSettings v-if="selectedModeCode == 'common'"/>
|
||||||
<CategoriesList v-else-if="selectedModeCode == 'categories'"/>
|
<CategoriesList v-else-if="selectedModeCode == 'categories'"/>
|
||||||
<RecurrentList v-else-if="selectedModeCode == 'recurrent'"/>
|
<RecurrentList v-else-if="selectedModeCode == 'recurrent'"/>
|
||||||
<p v-else-if="selectedModeCode == 'notifications'" class="flex justify-items-start justify-center items-center h-screen">NOTIFICATIONS UNDER CONSTRUCTIONS</p>
|
<p v-else-if="selectedModeCode == 'notifications'"
|
||||||
|
class="flex justify-items-start justify-center items-center h-screen">NOTIFICATIONS UNDER CONSTRUCTIONS</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
<div v-if="loading">
|
<div v-if="loading">
|
||||||
<LoadingView/>
|
<LoadingView/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex flex-col bg-gray-100 dark:bg-gray-800 h-screen p-4">
|
<div v-else class="flex flex-col bg-gray-100 pb-4 ">
|
||||||
<ConfirmDialog/>
|
<ConfirmDialog/>
|
||||||
<div v-if="!space">Выберите сперва пространство</div>
|
<div v-if="!space">Выберите сперва пространство</div>
|
||||||
<!-- Заголовок и кнопка добавления категории -->
|
<!-- Заголовок и кнопка добавления категории -->
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="flex flex-col gap-4 xl:flex-row justify-between bg-gray-100">
|
<div class="flex flex-col gap-4 xl:flex-row justify-between bg-gray-100">
|
||||||
<h2 class="text-5xl font-bold">Категории</h2>
|
<h2 class="text-4xl ">Категории</h2>
|
||||||
<Button label="Добавить категорию" icon="pi pi-plus" class="text-sm" @click="openCreateDialog(null)"/>
|
<Button label="Добавить категорию" icon="pi pi-plus" class="text-sm" @click="openCreateDialog(null)"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -21,62 +21,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Переключатель категорий (доходы/расходы) -->
|
<!-- Переключатель категорий (доходы/расходы) -->
|
||||||
<div class="card flex lg:hidden justify-center mb-4">
|
<div class="card flex justify-center w-full ">
|
||||||
<SelectButton v-model="selectedCategoryType" :options="categoryTypes" optionLabel="name"
|
<SelectButton v-model="selectedCategoryType" :options="categoryTypes" optionLabel="name"
|
||||||
|
class="!bg-emerald-50 !border-emerald-600 "
|
||||||
aria-labelledby="category-switch"/>
|
aria-labelledby="category-switch"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Список категорий с прокруткой для больших экранов -->
|
|
||||||
<div class="flex">
|
|
||||||
<div class="hidden lg:grid grid-cols-2 gap-x-10 w-full h-full justify-items-center overflow-y-auto">
|
|
||||||
<!-- Категории доходов -->
|
|
||||||
<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">Поступления</h3>
|
|
||||||
<Button icon="pi pi-plus" rounded outlined class="p-button-success"
|
|
||||||
@click="openCreateDialog('INCOME')"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class=" overflow-y-auto mt-2 space-y-2 px-2">
|
|
||||||
<CategoryListItem
|
|
||||||
class=""
|
|
||||||
v-for="category in filteredIncomeCategories"
|
|
||||||
:key="category.id"
|
|
||||||
:category="category"
|
|
||||||
v-bind="category"
|
|
||||||
@open-edit="openEdit"
|
|
||||||
@delete-category="confirmDelete"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Категории расходов -->
|
|
||||||
<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">Расходы</h3>
|
|
||||||
<Button icon="pi pi-plus" rounded outlined class="p-button-success !hover:bg-green-600"
|
|
||||||
@click="openCreateDialog('EXPENSE')"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class=" overflow-y-auto pb-10 mt-2 space-y-2 px-2">
|
|
||||||
<CategoryListItem
|
|
||||||
v-for="category in filteredExpenseCategories"
|
|
||||||
:key="category.id"
|
|
||||||
:category="category"
|
|
||||||
v-bind="category"
|
|
||||||
@open-edit="openEdit"
|
|
||||||
@delete-category="confirmDelete"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Для маленьких экранов -->
|
<!-- Для маленьких экранов -->
|
||||||
<div class="flex lg:hidden flex-wrap rounded w-full">
|
<div class=" flex flex-col md:grid md:grid-cols-2 lg:grid-cols-2 gap-1 justify-between">
|
||||||
<CategoryListItem
|
<CategoryListItem
|
||||||
v-for="category in filteredCategories"
|
v-for="category in filteredCategories"
|
||||||
:key="category.id"
|
:key="category.id"
|
||||||
@@ -84,13 +37,14 @@
|
|||||||
v-bind="category"
|
v-bind="category"
|
||||||
class="mt-2"
|
class="mt-2"
|
||||||
@open-edit="openEdit"
|
@open-edit="openEdit"
|
||||||
@delete-category="confirmDelete"
|
@delete-category="deleteCat"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CreateCategoryModal
|
<CreateCategoryModal
|
||||||
v-if="isDialogVisible"
|
v-if="isDialogVisible"
|
||||||
:show="isDialogVisible"
|
:show="isDialogVisible"
|
||||||
|
:show-tags="true"
|
||||||
:categoryTypes="categoryTypes"
|
:categoryTypes="categoryTypes"
|
||||||
:selectedCategoryType="selectedCategoryType"
|
:selectedCategoryType="selectedCategoryType"
|
||||||
:category="editingCategory"
|
:category="editingCategory"
|
||||||
@@ -113,8 +67,7 @@ import CreateCategoryModal from './CreateCategoryModal.vue';
|
|||||||
import CategoryListItem from '@/components/settings/categories/CategoryListItem.vue';
|
import CategoryListItem from '@/components/settings/categories/CategoryListItem.vue';
|
||||||
import {Category, CategoryType} from '@/models/Category';
|
import {Category, CategoryType} from '@/models/Category';
|
||||||
import {
|
import {
|
||||||
createCategory,
|
createCategory, deleteCategoryRequest,
|
||||||
deleteCategory,
|
|
||||||
getCategories,
|
getCategories,
|
||||||
getCategoryTypes,
|
getCategoryTypes,
|
||||||
} from "@/services/categoryService";
|
} from "@/services/categoryService";
|
||||||
@@ -132,8 +85,6 @@ const expenseCategories = ref<Category[]>([]);
|
|||||||
const incomeCategories = ref<Category[]>([]);
|
const incomeCategories = ref<Category[]>([]);
|
||||||
const editingCategory = ref<Category | null>(null);
|
const editingCategory = ref<Category | null>(null);
|
||||||
const isDialogVisible = ref(false);
|
const isDialogVisible = ref(false);
|
||||||
const confirm = useConfirm();
|
|
||||||
const toast = useToast();
|
|
||||||
|
|
||||||
const fetchCategories = async () => {
|
const fetchCategories = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@@ -205,37 +156,11 @@ const saveCategory = async (newCategory: Category) => {
|
|||||||
closeCreateDialog();
|
closeCreateDialog();
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmDelete = async (category: Category) => {
|
|
||||||
|
|
||||||
confirm.require({
|
|
||||||
message: 'Вы уверены, что хотите выполнить это действие?\n Это нельзя будет отменить.\nВсе транзакции данной категории будут перенесены в категорию "Другое".\n',
|
|
||||||
header: `Удаление категории ${category.name}`,
|
|
||||||
icon: 'pi pi-info-circle',
|
|
||||||
rejectLabel: 'Cancel',
|
|
||||||
rejectProps: {
|
|
||||||
label: 'Cancel',
|
|
||||||
severity: 'secondary',
|
|
||||||
outlined: true
|
|
||||||
},
|
|
||||||
acceptProps: {
|
|
||||||
label: 'Delete',
|
|
||||||
severity: 'danger'
|
|
||||||
},
|
|
||||||
accept: async () => {
|
|
||||||
await deleteCategory(category.id);
|
|
||||||
await fetchCategories();
|
|
||||||
toast.add({severity: 'info', summary: 'Confirmed', detail: 'Record deleted', life: 3000});
|
|
||||||
},
|
|
||||||
reject: () => {
|
|
||||||
toast.add({severity: 'error', summary: 'Rejected', detail: 'You have rejected', life: 3000});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteCat = async (categoryId: number) => {
|
const deleteCat = async (categoryId: number) => {
|
||||||
|
|
||||||
await deleteCategory(categoryId);
|
// await deleteCategory(categoryId);
|
||||||
await fetchCategories();
|
await fetchCategories();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,19 +193,22 @@ watch(
|
|||||||
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
})
|
})
|
||||||
|
await fetchCategoryTypes();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching budget infos:', error);
|
console.error('Error fetching budget infos:', error);
|
||||||
}
|
}
|
||||||
}})
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (space.value) {
|
if (space.value) {
|
||||||
await fetchCategories();
|
await fetchCategories();
|
||||||
|
console.log("here");
|
||||||
await fetchCategoryTypes();
|
await fetchCategoryTypes();
|
||||||
}
|
}
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
214
src/components/settings/categories/CategoryCreationView.vue
Normal file
214
src/components/settings/categories/CategoryCreationView.vue
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
|
||||||
|
import {ref, watch, computed, PropType, onMounted, defineProps} from 'vue';
|
||||||
|
import Button from "primevue/button";
|
||||||
|
import SelectButton from "primevue/selectbutton";
|
||||||
|
import {Category, CategoryType} from '@/models/Category';
|
||||||
|
import Tag from "primevue/tag";
|
||||||
|
import {useSpaceStore} from "@/stores/spaceStore";
|
||||||
|
import {createCategory, editCategoryRequest, getTagsRequest} from "@/services/categoryService";
|
||||||
|
import {useToast} from "primevue/usetoast";
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
const props = defineProps({
|
||||||
|
categoryTypes: Object as PropType<CategoryType[]>,
|
||||||
|
categoryProp: Object as PropType<Category | null>, // Категория для редактирования (null, если создание)
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['saveCategory', 'close-modal'])
|
||||||
|
const category = ref<Category>(props.categoryProp)
|
||||||
|
const icon = ref('🐱');
|
||||||
|
const name = ref('');
|
||||||
|
const description = ref('');
|
||||||
|
const showEmojiPicker = ref(false);
|
||||||
|
const categoryType = ref(props.categoryProp?.type || props.categoryTypes[0]); // Тип по умолчанию
|
||||||
|
const isEditing = computed(() => !!props.categoryProp); // Если есть категория, значит редактирование
|
||||||
|
const availableTags = computed(() => {
|
||||||
|
const availableTags = []
|
||||||
|
tags.value.forEach((tag1) => {
|
||||||
|
console.log(tag1.code)
|
||||||
|
console.log(category.value.tags)
|
||||||
|
if (category.value?.tags.filter(tag => tag.code == tag1.code).length == 0) {
|
||||||
|
availableTags.push(tag1);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return availableTags;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const emojis = ['😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂', '🙂', '🙃', '😉', '😊', '😇', '😍', '🥰', '😘', '🍎', '🍔', '🍕', '🍣', '☕', '🍺', '🚗', '🚕', '🚴♂️', '🚆', '✈️', '⛴️', '🛒', '👗', '💍', '👟', '🛍️', '🎮', '🎥', '🎧', '🎢', '🎨', '🏠', '🛏️', '🧹', '🪴', '🍼', '🏥', '💊', '🩺', '🦷', '💳', '💰', '🏦', '🌍', '🗺️', '🏝️', '🏔️', '💻', '📚', '🖋️', '🏫'];
|
||||||
|
|
||||||
|
const toggleEmojiPicker = () => {
|
||||||
|
showEmojiPicker.value = !showEmojiPicker.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectEmoji = (emoji: string) => {
|
||||||
|
category.value.icon = emoji
|
||||||
|
showEmojiPicker.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveCategory = async () => {
|
||||||
|
// const categoryData = new Category(categoryType.value, name.value, description.value, icon.value);
|
||||||
|
if (isEditing.value) {
|
||||||
|
console.log(category.value)
|
||||||
|
await editCategoryRequest(category.value).then((res) => {
|
||||||
|
category.value = res.data
|
||||||
|
emit("saveCategory", category.value)
|
||||||
|
}).catch(err =>
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка сохранения категории',
|
||||||
|
detail: err.response.data.message,
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
await createCategory(category.value).then((res) => {
|
||||||
|
category.value = res.data
|
||||||
|
emit("saveCategory", category.value)
|
||||||
|
}).catch(err =>
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка создания категории',
|
||||||
|
detail: err.response.data.message,
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
resetForm();
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
emit('close-modal');
|
||||||
|
resetForm();
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
name.value = '';
|
||||||
|
description.value = '';
|
||||||
|
icon.value = '🐱';
|
||||||
|
categoryType.value = 'EXPENSE';
|
||||||
|
showEmojiPicker.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addTag = (tag: CategoryType) => {
|
||||||
|
category.value.tags.push(tag);
|
||||||
|
}
|
||||||
|
const deleteTag = (tag) => {
|
||||||
|
category.value.tags = category.value.tags.filter(t => t.code !== tag.code);
|
||||||
|
}
|
||||||
|
const tags = ref<CategoryType[]>([]);
|
||||||
|
const spaceStore = useSpaceStore()
|
||||||
|
const selectedSpace = computed(() => spaceStore.space)
|
||||||
|
|
||||||
|
const fetchTags = async () => {
|
||||||
|
await getTagsRequest().then(res => tags.value = res)
|
||||||
|
.catch(err => {
|
||||||
|
console.log(err);
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка получения тэгов',
|
||||||
|
detail: err.response.data.message,
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => selectedSpace.value,
|
||||||
|
async (newValue, oldValue) => {
|
||||||
|
|
||||||
|
if (newValue != oldValue || !oldValue) {
|
||||||
|
try {
|
||||||
|
// loading.value = true;
|
||||||
|
// Если выбранный space изменился, получаем новую информацию о бюджете
|
||||||
|
if (selectedSpace.value) {
|
||||||
|
await fetchTags();
|
||||||
|
}
|
||||||
|
constructCategory()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching budget infos:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const constructCategory = () => {
|
||||||
|
if (props.categoryProp) {
|
||||||
|
category.value = new Category(props.categoryProp.id, props.categoryProp.type, props.categoryProp.name, props.categoryProp.description, props.categoryProp.icon, props.categoryProp.tags);
|
||||||
|
} else {
|
||||||
|
category.value = new Category(null, categoryType.value, '', '', icon.value, []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
console.log(props)
|
||||||
|
|
||||||
|
|
||||||
|
constructCategory()
|
||||||
|
// console.log(props.category)
|
||||||
|
if (selectedSpace.value) {
|
||||||
|
await fetchTags();
|
||||||
|
}
|
||||||
|
console.log(category.value);
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex justify-center gap-4">
|
||||||
|
<!-- <Button rounded outlined @click="toggleEmojiPicker" class=" flex justify-center !rounded-full !text-4xl !p-4"-->
|
||||||
|
<!-- size="large" :label="category.icon"/>-->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ category }}
|
||||||
|
{{props.categoryTypes}}
|
||||||
|
{{categoryType}}
|
||||||
|
<div
|
||||||
|
class="absolute pt-1 border rounded-lg shadow-2xl border-gray-300 bg-white grid grid-cols-6 gap-4 h-40 z-50 ml-3 mt-1 overflow-scroll"
|
||||||
|
v-if="showEmojiPicker">
|
||||||
|
<Button v-for="emoji in emojis" class="!p-4" @click="selectEmoji(emoji)" :label="emoji" text/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SelectButton для выбора типа категории -->
|
||||||
|
<div class="flex justify-center mt-4">
|
||||||
|
<!-- <SelectButton v-if="!isEditing" v-model="category.type" :options="categoryTypes" optionLabel="name"/>-->
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<!-- Поля для создания/редактирования категории -->
|
||||||
|
<label for="newCategoryName">Название категории:</label>
|
||||||
|
<!-- <input v-modezl="category.name" type="text" id="newCategoryName"/>-->
|
||||||
|
|
||||||
|
<label for="newCategoryDesc">Описание категории:</label>
|
||||||
|
<!-- <input v-model="category.description" type="text" id="newCategoryDesc"/>-->
|
||||||
|
<div class="flex flex-col flex-wrap gap-0">
|
||||||
|
<h2 class="text-lg ">Добавленные теги</h2>
|
||||||
|
<div class="flex flex-row flex-wrap gap-2">
|
||||||
|
<!-- <Tag v-if="category.tags" v-for="tag in category.tags" :key="tag.id" class="w-fit group">{{ tag.name }}-->
|
||||||
|
<!-- ({{ tag.code }})<i-->
|
||||||
|
<!-- class="pi pi-trash !hidden group-hover:!block" @click="deleteTag(tag)"/></Tag>-->
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col flex-wrap gap-0">
|
||||||
|
<h2 class="text-lg ">Доступные теги</h2>
|
||||||
|
<div class="flex flex-row flex-wrap gap-2">
|
||||||
|
<!-- <Tag v-for="tag in availableTags" :key="tag.id" class="w-fit " @click="addTag(tag)">-->
|
||||||
|
<!-- {{ tag.name }} ({{ tag.code }})-->
|
||||||
|
<!-- </Tag>-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Кнопки -->
|
||||||
|
<div class="button-group">
|
||||||
|
<button @click="saveCategory" class="create-category-btn">{{ isEditing ? 'Сохранить' : 'Создать' }}</button>
|
||||||
|
<button @click="closeModal" class="close-modal-btn">Отмена</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -1,13 +1,19 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Category } from "@/models/Category";
|
import {Category} from "@/models/Category";
|
||||||
import { PropType } from "vue";
|
import {PropType} from "vue";
|
||||||
import Button from "primevue/button";
|
import Button from "primevue/button";
|
||||||
|
import Tag from "primevue/tag";
|
||||||
|
import {deleteCategoryRequest} from "@/services/categoryService";
|
||||||
|
import {useConfirm} from "primevue/useconfirm";
|
||||||
|
import {useToast} from "primevue/usetoast";
|
||||||
|
|
||||||
|
|
||||||
// Определение входных параметров (props)
|
// Определение входных параметров (props)
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
category: { type: Object as PropType<Category>, required: true }
|
category: {type: Object as PropType<Category>, required: true}
|
||||||
});
|
});
|
||||||
|
const confirm = useConfirm();
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
// Определение событий (emits)
|
// Определение событий (emits)
|
||||||
const emit = defineEmits(["open-edit", "delete-category"]);
|
const emit = defineEmits(["open-edit", "delete-category"]);
|
||||||
@@ -18,15 +24,43 @@ const openEdit = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Функция для удаления категории
|
// Функция для удаления категории
|
||||||
const deleteCategory = () => {
|
const confirmDelete = async () => {
|
||||||
|
confirm.require({
|
||||||
|
message: 'Вы уверены, что хотите выполнить это действие?\n Это нельзя будет отменить.\nВсе транзакции данной категории будут перенесены в категорию "Другое".\n',
|
||||||
|
header: `Удаление категории ${props.category.name}`,
|
||||||
|
icon: 'pi pi-info-circle',
|
||||||
|
rejectLabel: 'Cancel',
|
||||||
|
rejectProps: {
|
||||||
|
label: 'Cancel',
|
||||||
|
severity: 'secondary',
|
||||||
|
outlined: true
|
||||||
|
},
|
||||||
|
acceptProps: {
|
||||||
|
label: 'Delete',
|
||||||
|
severity: 'danger'
|
||||||
|
},
|
||||||
|
accept: async () => {
|
||||||
|
await deleteCategoryRequest(props.category.id).then((result) => {
|
||||||
|
emit("delete-category", props.category);
|
||||||
|
toast.add({severity: 'info', summary: 'Confirmed', detail: 'Record deleted', life: 3000});
|
||||||
|
}).catch(((err) => {
|
||||||
|
toast.add({severity: 'error', summary: 'Rejected', detail: 'Ошибка удаления категории', life: 3000});
|
||||||
|
})
|
||||||
|
)
|
||||||
|
},
|
||||||
|
reject: () => {
|
||||||
|
// toast.add({severity: 'error', summary: 'Rejected', detail: 'You have rejected', life: 3000});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
emit("delete-category", props.category); // Использование события для удаления категории
|
|
||||||
};
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div 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-col rounded-xl border-2 bg-white shadow-xl min-w-fit max-h-fit items-center justify-between w-full p-2 gap-2">
|
||||||
|
<div class="flex items-center justify-between w-full">
|
||||||
<div class="flex flex-row items-center p-x-4 gap-4">
|
<div class="flex flex-row items-center p-x-4 gap-4">
|
||||||
<p class="text-6xl font-bold text-gray-700 dark:text-gray-400">{{ category.icon }}</p>
|
<p class="text-6xl font-bold text-gray-700 dark:text-gray-400">{{ category.icon }}</p>
|
||||||
<div class="flex flex-col items-start justify-items-start w-full">
|
<div class="flex flex-col items-start justify-items-start w-full">
|
||||||
@@ -34,10 +68,15 @@ const deleteCategory = () => {
|
|||||||
<p class="font-light line-clamp-1">{{ category.description }}</p>
|
<p class="font-light line-clamp-1">{{ category.description }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-row items-center px-4 gap-2 ">
|
||||||
<div class="flex flex-row items-center p-x-4 gap-2 ">
|
<button @click="openEdit"><i class="pi pi-pen-to-square"/></button>
|
||||||
<Button icon="pi pi-pen-to-square" rounded @click="openEdit"/>
|
<button @click="confirmDelete"><i class="pi pi-trash"/></button>
|
||||||
<!-- <Button icon="pi pi-trash" severity="danger" rounded @click="deleteCategory"/>-->
|
<!-- <Button icon="pi pi-trash" severity="danger" rounded @click="deleteCategory"/>-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row items-start w-full justify-start p-x-4 gap-2 ">
|
||||||
|
<Tag v-if="category.tags.length>0" v-for="tag in category.tags">{{ tag.name }} ({{ tag.code }})</Tag>
|
||||||
|
<span class="p-1"> </span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dialog :visible="show" modal :header="isEditing ? 'Edit Category' : 'Create New Category'" :closable="false"
|
<Dialog :visible="show" modal :header="isEditing ? 'Edit Category' : 'Create New Category'" :closable="false"
|
||||||
class="w-[95%] xl:!w-1/3">
|
class="w-[95%] xl:!w-1/3">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
<div class="flex justify-center gap-4">
|
<div class="flex justify-center gap-4">
|
||||||
<Button rounded outlined @click="toggleEmojiPicker" class=" flex justify-center !rounded-full !text-4xl !p-4"
|
<Button rounded outlined @click="toggleEmojiPicker" class=" flex justify-center !rounded-full !text-4xl !p-4"
|
||||||
size="large" :label="category.icon"/>
|
size="large" :label="category.icon"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- {{ // category }}-->
|
||||||
|
<!-- {{props.categoryTypes}}-->
|
||||||
|
<!-- {{categoryType}}-->
|
||||||
<div
|
<div
|
||||||
class="absolute pt-1 border rounded-lg shadow-2xl border-gray-300 bg-white grid grid-cols-6 gap-4 h-40 z-50 ml-3 mt-1 overflow-scroll"
|
class="absolute pt-1 border rounded-lg shadow-2xl border-gray-300 bg-white grid grid-cols-6 gap-4 h-40 z-50 ml-3 mt-1 overflow-scroll"
|
||||||
v-if="showEmojiPicker">
|
v-if="showEmojiPicker">
|
||||||
@@ -17,14 +20,14 @@
|
|||||||
<div class="flex justify-center mt-4">
|
<div class="flex justify-center mt-4">
|
||||||
<SelectButton v-if="!isEditing" v-model="category.type" :options="categoryTypes" optionLabel="name"/>
|
<SelectButton v-if="!isEditing" v-model="category.type" :options="categoryTypes" optionLabel="name"/>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
<!-- Поля для создания/редактирования категории -->
|
<!-- Поля для создания/редактирования категории -->
|
||||||
<label for="newCategoryName">Название категории:</label>
|
<label for="newCategoryName">Название категории:</label>
|
||||||
<input v-model="category.name" type="text" id="newCategoryName"/>
|
<input v-model="category.name" type="text" id="newCategoryName"/>
|
||||||
|
|
||||||
<label for="newCategoryDesc">Описание категории:</label>
|
<label for="newCategoryDesc">Описание категории:</label>
|
||||||
<input v-model="category.description" type="text" id="newCategoryDesc"/>
|
<input v-model="category.description" type="text" id="newCategoryDesc"/>
|
||||||
<div class="flex flex-col flex-wrap gap-0">
|
<div v-if="showTags" class="flex flex-col flex-wrap gap-0">
|
||||||
<h2 class="text-lg ">Добавленные теги</h2>
|
<h2 class="text-lg ">Добавленные теги</h2>
|
||||||
<div class="flex flex-row flex-wrap gap-2">
|
<div class="flex flex-row flex-wrap gap-2">
|
||||||
<Tag v-if="category.tags" v-for="tag in category.tags" :key="tag.id" class="w-fit group">{{ tag.name }}
|
<Tag v-if="category.tags" v-for="tag in category.tags" :key="tag.id" class="w-fit group">{{ tag.name }}
|
||||||
@@ -32,8 +35,8 @@
|
|||||||
class="pi pi-trash !hidden group-hover:!block" @click="deleteTag(tag)"/></Tag>
|
class="pi pi-trash !hidden group-hover:!block" @click="deleteTag(tag)"/></Tag>
|
||||||
<span></span>
|
<span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="flex flex-col flex-wrap gap-2">
|
<div class="flex flex-col flex-wrap gap-0">
|
||||||
<h2 class="text-lg ">Доступные теги</h2>
|
<h2 class="text-lg ">Доступные теги</h2>
|
||||||
<div class="flex flex-row flex-wrap gap-2">
|
<div class="flex flex-row flex-wrap gap-2">
|
||||||
<Tag v-for="tag in availableTags" :key="tag.id" class="w-fit " @click="addTag(tag)">
|
<Tag v-for="tag in availableTags" :key="tag.id" class="w-fit " @click="addTag(tag)">
|
||||||
@@ -41,11 +44,14 @@
|
|||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Кнопки -->
|
<!-- Кнопки -->
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
<button @click="saveCategory" class="create-category-btn">{{ isEditing ? 'Сохранить' : 'Создать' }}</button>
|
<button @click="saveCategory" class="create-category-btn">{{ isEditing ? 'Сохранить' : 'Создать' }}</button>
|
||||||
<button @click="closeModal" class="close-modal-btn">Отмена</button>
|
<button @click="closeModal" class="close-modal-btn">Отмена</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -59,11 +65,15 @@ import Tag from "primevue/tag";
|
|||||||
import {useSpaceStore} from "@/stores/spaceStore";
|
import {useSpaceStore} from "@/stores/spaceStore";
|
||||||
import {createCategory, editCategoryRequest, getTagsRequest} from "@/services/categoryService";
|
import {createCategory, editCategoryRequest, getTagsRequest} from "@/services/categoryService";
|
||||||
import {useToast} from "primevue/usetoast";
|
import {useToast} from "primevue/usetoast";
|
||||||
|
import {useConfirm} from "primevue/useconfirm";
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
const confirm = useConfirm();
|
||||||
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
show: Boolean,
|
show: Boolean,
|
||||||
|
showTags: Boolean,
|
||||||
categoryTypes: Object as PropType<CategoryType[]>,
|
categoryTypes: Object as PropType<CategoryType[]>,
|
||||||
category: Object as PropType<Category | null>, // Категория для редактирования (null, если создание)
|
category: Object as PropType<Category | null>, // Категория для редактирования (null, если создание)
|
||||||
})
|
})
|
||||||
@@ -107,11 +117,17 @@ const saveCategory = async () => {
|
|||||||
category.value = res.data
|
category.value = res.data
|
||||||
emit("saveCategory", category.value)
|
emit("saveCategory", category.value)
|
||||||
}).catch(err =>
|
}).catch(err =>
|
||||||
toast.add({severity: 'error', summary: 'Ошибка сохранения категории', detail: err.response.data.message, life: 3000})
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка сохранения категории',
|
||||||
|
detail: err.response.data.message,
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
await createCategory(category.value).then((res) => {
|
await createCategory(category.value).then((res) => {
|
||||||
category.value = res.data
|
category.value = res.data
|
||||||
|
emit("saveCategory", category.value)
|
||||||
}).catch(err =>
|
}).catch(err =>
|
||||||
toast.add({
|
toast.add({
|
||||||
severity: 'error',
|
severity: 'error',
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dialog :visible="show" modal :header="isEditing ? 'Edit Recurrent Payment' : 'Create Recurrent Payment'"
|
<Dialog :visible="show" modal :header="isEditing ? 'Редактировать повторяющийся платеж' : 'Создать повторяющийся платеж'"
|
||||||
:closable="true" class="!w-1/3">
|
:closable="true" class="!w-5/6 xl:!w-2/4">
|
||||||
<div v-if="loading">
|
<div v-if="loading">
|
||||||
Loading...
|
Loading...
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="p-fluid flex flex-col gap-6 w-full py-6 items-start">
|
<div v-else class="p-fluid flex flex-col gap-6 w-full py-6 items-start">
|
||||||
<!-- Название -->
|
<!-- Название -->
|
||||||
<FloatLabel class="w-full">
|
<FloatLabel class="w-full" variant="on">
|
||||||
<label for="paymentName">Payment Name</label>
|
<label for="paymentName">Название платежа</label>
|
||||||
<InputText v-model="name" id="paymentName" class="!w-full"/>
|
<InputText v-model="name" id="paymentName" class="!w-full"/>
|
||||||
</FloatLabel>
|
</FloatLabel>
|
||||||
|
|
||||||
<!-- Категория -->
|
<!-- Категория -->
|
||||||
<div class="relative w-full justify-center justify-items-center ">
|
<div class="relative w-full justify-center justify-items-center ">
|
||||||
<div class="flex flex-col justify-items-center gap-2">
|
<div class="flex flex-col justify-items-center gap-2 w-full">
|
||||||
<SelectButton v-model="selectedCategoryType" :options="categoryTypes" optionLabel="name"
|
<SelectButton v-model="selectedCategoryType" :options="categoryTypes" optionLabel="name"
|
||||||
aria-labelledby="basic"
|
aria-labelledby="basic"
|
||||||
@change="categoryTypeChanged(selectedCategoryType.code)" class="justify-center"/>
|
@change="categoryTypeChanged(selectedCategoryType.code)" class="justify-start w-fit"/>
|
||||||
<button class="border border-gray-300 rounded-lg w-full z-50"
|
<button class="border border-gray-300 rounded-lg w-full z-50"
|
||||||
@click="isCategorySelectorOpened = !isCategorySelectorOpened">
|
@click="isCategorySelectorOpened = !isCategorySelectorOpened">
|
||||||
<div class="flex flex-row items-center pe-4 py-2 gap-4">
|
<div class="flex flex-row items-center pe-4 py-2 gap-4">
|
||||||
<div class="flex flex-row justify-between w-full px-4">
|
<div class="flex flex-row justify-between items-center w-full px-4 gap-4">
|
||||||
<p class="text-6xl font-bold text-gray-700 dark:text-gray-400">{{ selectedCategory.icon }}</p>
|
<p class="text-3xl font-bold text-gray-700 dark:text-gray-400">{{ selectedCategory.icon }}</p>
|
||||||
<div class="flex flex-col items-start justify-items-start justify-around w-full">
|
<div class="flex flex-col items-start justify-items-start justify-around w-full">
|
||||||
<p class="font-bold text-start">{{ selectedCategory.name }}</p>
|
<p class="font-bold text-start">{{ selectedCategory.name }}</p>
|
||||||
<p class="font-light line-clamp-1 items-start text-start">{{ selectedCategory.description }}</p>
|
<p class="font-light line-clamp-1 items-start text-start">{{ selectedCategory.description }}</p>
|
||||||
@@ -38,12 +38,12 @@
|
|||||||
<!-- Анимированное открытие списка категорий -->
|
<!-- Анимированное открытие списка категорий -->
|
||||||
<div v-show="isCategorySelectorOpened"
|
<div v-show="isCategorySelectorOpened"
|
||||||
class="absolute left-0 right-0 top-full overflow-hidden z-50 border-b-4 border-x rounded-b-lg bg-white shadow-lg transition-all duration-500"
|
class="absolute left-0 right-0 top-full overflow-hidden 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 }">
|
:class="{ 'max-h-0': !isCategorySelectorOpened, '': isCategorySelectorOpened }">
|
||||||
<div class="grid grid-cols-2 mt-2">
|
<div class="grid grid-cols-2 mt-2">
|
||||||
<button v-for="category in selectedCategoryType.code == 'EXPENSE' ? expenseCategories : incomeCategories"
|
<button v-for="category in selectedCategoryType.code == 'EXPENSE' ? expenseCategories : incomeCategories"
|
||||||
:key="category.id" class="border rounded-lg mx-2 mb-2"
|
:key="category.id" class="border rounded-lg mx-2 mb-2"
|
||||||
@click="selectCategory(category)">
|
@click="selectCategory(category)">
|
||||||
<div class="flex flex-row justify-between w-full px-2">
|
<div class="flex flex-row justify-between w-full p-1 gap-2">
|
||||||
<p class="text-4xl font-bold text-gray-700 dark:text-gray-400">{{ category.icon }}</p>
|
<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">
|
<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-bold text-start">{{ category.name }}</p>
|
||||||
@@ -56,9 +56,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Описание -->
|
<!-- Описание -->
|
||||||
<FloatLabel class="w-full">
|
<FloatLabel class="!w-full">
|
||||||
<label for="description">Description</label>
|
<label for="description">Описание</label>
|
||||||
<Textarea v-model="description" id="description" rows="3"/>
|
<Textarea v-model="description" id="description" rows="3" class="!w-full"/>
|
||||||
</FloatLabel>
|
</FloatLabel>
|
||||||
|
|
||||||
<!-- Дата повторения (выпадающий список) -->
|
<!-- Дата повторения (выпадающий список) -->
|
||||||
@@ -92,14 +92,14 @@
|
|||||||
<!-- Сумма -->
|
<!-- Сумма -->
|
||||||
<InputGroup class="w-full">
|
<InputGroup class="w-full">
|
||||||
<InputGroupAddon>₽</InputGroupAddon>
|
<InputGroupAddon>₽</InputGroupAddon>
|
||||||
<InputNumber v-model="amount" placeholder="Amount"/>
|
<InputNumber v-model="amount" placeholder="Сумма"/>
|
||||||
<InputGroupAddon>.00</InputGroupAddon>
|
<InputGroupAddon>.00</InputGroupAddon>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
|
||||||
<!-- Кнопки -->
|
<!-- Кнопки -->
|
||||||
<div class="flex justify-content-end gap-2 mt-4">
|
<div class="flex justify-content-end gap-2 mt-4">
|
||||||
<Button label="Save" icon="pi pi-check" @click="savePayment" class="p-button-success"/>
|
<Button label="Сохранить" icon="pi pi-check" @click="savePayment" class="p-button-success"/>
|
||||||
<Button label="Cancel" icon="pi pi-times" @click="closeModal" class="p-button-secondary"/>
|
<Button label="Отмена" icon="pi pi-times" @click="closeModal" class="p-button-secondary"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@@ -133,7 +133,8 @@ const loading = ref(false)
|
|||||||
// Поля для формы
|
// Поля для формы
|
||||||
const name = ref('');
|
const name = ref('');
|
||||||
const selectedCategoryType = ref(props.payment ? props.payment.type : props.categoryTypes[0]);
|
const selectedCategoryType = ref(props.payment ? props.payment.type : props.categoryTypes[0]);
|
||||||
const selectedCategory = ref(selectedCategoryType.code == 'EXPESE' ? props.expenseCategories[0] : props.incomeCategories[0]);
|
console.log(props.categoryTypes)
|
||||||
|
const selectedCategory = ref(selectedCategoryType.code == 'EXPENSE' ? props.expenseCategories[0] : props.incomeCategories[0]);
|
||||||
|
|
||||||
const categoryTypeChanged = (code) => {
|
const categoryTypeChanged = (code) => {
|
||||||
|
|
||||||
|
|||||||
220
src/components/settings/recurrent/RecurrentCreationView.vue
Normal file
220
src/components/settings/recurrent/RecurrentCreationView.vue
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import InputText from "primevue/inputtext";
|
||||||
|
import SelectButton from "primevue/selectbutton";
|
||||||
|
import Button from "primevue/button";
|
||||||
|
import InputGroupAddon from "primevue/inputgroupaddon";
|
||||||
|
import FloatLabel from "primevue/floatlabel";
|
||||||
|
import Textarea from "primevue/textarea";
|
||||||
|
import InputGroup from "primevue/inputgroup";
|
||||||
|
import InputNumber from "primevue/inputnumber";
|
||||||
|
import {computed, defineEmits, ref, watch} from "vue";
|
||||||
|
import {saveRecurrentPayment} from "@/services/recurrentService";
|
||||||
|
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
show: Boolean, // Показать/скрыть модальное окно
|
||||||
|
expenseCategories: Array, // Внешние данные для списка категорий
|
||||||
|
incomeCategories: Array, // Внешние данные для списка категорий
|
||||||
|
categoryTypes: Array,
|
||||||
|
payment: Object | null // Для редактирования существующего платежа
|
||||||
|
})
|
||||||
|
|
||||||
|
const emits = defineEmits(["open-edit", "save-payment", "close-modal"]);
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
// Поля для формы
|
||||||
|
const name = ref('');
|
||||||
|
const selectedCategoryType = ref(props.payment ? props.payment.type : props.categoryTypes[0]);
|
||||||
|
const selectedCategory = ref(selectedCategoryType.code == 'EXPESE' ? props.expenseCategories[0] : props.incomeCategories[0]);
|
||||||
|
|
||||||
|
const categoryTypeChanged = (code) => {
|
||||||
|
|
||||||
|
selectedCategory.value = code == "EXPENSE" ? props.expenseCategories[0] : props.incomeCategories[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const description = ref('');
|
||||||
|
const repeatDay = ref<number | null>(null);
|
||||||
|
const amount = ref<number | null>(null);
|
||||||
|
|
||||||
|
// Открытие/закрытие списка категорий
|
||||||
|
const isCategorySelectorOpened = ref(false);
|
||||||
|
const isDaySelectorOpened = ref(false);
|
||||||
|
|
||||||
|
// Список дней (1–31)
|
||||||
|
const days = Array.from({length: 31}, (_, i) => i + 1);
|
||||||
|
|
||||||
|
// Выбор дня
|
||||||
|
const selectDay = (day: number) => {
|
||||||
|
repeatDay.value = day;
|
||||||
|
isDaySelectorOpened.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Выбор категории
|
||||||
|
const selectCategory = (category) => {
|
||||||
|
isCategorySelectorOpened.value = false;
|
||||||
|
selectedCategory.value = category;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Определение, редактируем ли мы существующий платеж
|
||||||
|
const isEditing = computed(() => !!props.payment);
|
||||||
|
|
||||||
|
// Слушаем изменения, если редактируем существующий платеж
|
||||||
|
watch(() => props.payment, (newPayment) => {
|
||||||
|
if (newPayment) {
|
||||||
|
name.value = newPayment.name;
|
||||||
|
selectedCategory.value = newPayment.category;
|
||||||
|
description.value = newPayment.description;
|
||||||
|
repeatDay.value = newPayment.repeatDay;
|
||||||
|
amount.value = newPayment.amount;
|
||||||
|
} else {
|
||||||
|
resetForm();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Функция для сохранения платежа
|
||||||
|
const savePayment = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
const paymentData = {
|
||||||
|
name: name.value,
|
||||||
|
category: selectedCategory.value,
|
||||||
|
description: description.value,
|
||||||
|
atDay: repeatDay.value,
|
||||||
|
amount: amount.value
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isEditing.value && props.payment) {
|
||||||
|
paymentData.id = props.payment.id; // Если редактируем, сохраняем ID
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await saveRecurrentPayment(paymentData)
|
||||||
|
loading.value = false
|
||||||
|
resetForm();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving payment:', error);
|
||||||
|
}
|
||||||
|
emits('save-payment', paymentData);
|
||||||
|
resetForm();
|
||||||
|
closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Закрытие окна и сброс формы
|
||||||
|
const closeModal = () => {
|
||||||
|
emits('close-modal');
|
||||||
|
resetForm();
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
name.value = '';
|
||||||
|
selectedCategory.value = props.expenseCategories[0];
|
||||||
|
description.value = '';
|
||||||
|
repeatDay.value = null;
|
||||||
|
amount.value = null;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<div class="p-fluid flex flex-col gap-6 w-full py-6 items-start">
|
||||||
|
<!-- Название -->
|
||||||
|
<FloatLabel class="w-full" variant="on">
|
||||||
|
<label for="paymentName">Название платежа</label>
|
||||||
|
<InputText v-model="name" id="paymentName" class="!w-full"/>
|
||||||
|
</FloatLabel>
|
||||||
|
|
||||||
|
<!-- Категория -->
|
||||||
|
<div class="relative w-full justify-center justify-items-center ">
|
||||||
|
<div class="flex flex-col justify-items-center gap-2 w-full">
|
||||||
|
<SelectButton v-model="selectedCategoryType" :options="categoryTypes" optionLabel="name"
|
||||||
|
aria-labelledby="basic"
|
||||||
|
@change="categoryTypeChanged(selectedCategoryType.code)" class="justify-start"/>
|
||||||
|
<button class="border border-gray-300 rounded-lg w-full z-50"
|
||||||
|
@click="isCategorySelectorOpened = !isCategorySelectorOpened">
|
||||||
|
<div class="flex flex-row items-center pe-4 py-2 gap-4">
|
||||||
|
<div class="flex flex-row justify-between items-center w-full px-4 gap-4">
|
||||||
|
<p class="text-3xl font-bold text-gray-700 dark:text-gray-400">{{ selectedCategory.icon }}</p>
|
||||||
|
<div class="flex flex-col items-start justify-items-start justify-around w-full">
|
||||||
|
<p class="font-bold text-start">{{ selectedCategory.name }}</p>
|
||||||
|
<p class="font-light line-clamp-1 items-start text-start">{{ selectedCategory.description }}</p>
|
||||||
|
</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-hidden z-50 border-b-4 border-x rounded-b-lg bg-white shadow-lg transition-all duration-500"
|
||||||
|
:class="{ 'max-h-0': !isCategorySelectorOpened, '': isCategorySelectorOpened }">
|
||||||
|
<div class="grid grid-cols-2 mt-2">
|
||||||
|
<button v-for="category in selectedCategoryType.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 p-1 gap-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>
|
||||||
|
|
||||||
|
<!-- Описание -->
|
||||||
|
<FloatLabel class="!w-full">
|
||||||
|
<label for="description">Описание</label>
|
||||||
|
<Textarea v-model="description" id="description" rows="3" class="!w-full"/>
|
||||||
|
</FloatLabel>
|
||||||
|
|
||||||
|
<!-- Дата повторения (выпадающий список) -->
|
||||||
|
<div class="w-full relative">
|
||||||
|
<button class="border border-gray-300 rounded-lg w-full z-50"
|
||||||
|
@click="isDaySelectorOpened = !isDaySelectorOpened">
|
||||||
|
<div class="flex flex-row items-center pe-4 py-2 gap-4">
|
||||||
|
<div class="flex flex-row justify-between w-full px-4">
|
||||||
|
<p class="font-bold">Повторять каждый {{ repeatDay || 'N' }} день месяца</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span :class="{'rotate-90': isDaySelectorOpened}"
|
||||||
|
class="pi pi-angle-right transition-transform duration-300 text-5xl"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Анимированное открытие списка дней -->
|
||||||
|
<div v-show="isDaySelectorOpened"
|
||||||
|
class="absolute left-0 right-0 top-full overflow-hidden z-50 border-b-4 border-x rounded-b-lg bg-white shadow-lg transition-all duration-500"
|
||||||
|
:class="{ 'max-h-0': !isDaySelectorOpened, 'max-h-[500px]': isDaySelectorOpened }">
|
||||||
|
<div class="grid grid-cols-7 p-2">
|
||||||
|
<button v-for="day in days" :key="day" class=" border"
|
||||||
|
@click="selectDay(day)">
|
||||||
|
{{ day }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Сумма -->
|
||||||
|
<InputGroup class="w-full">
|
||||||
|
<InputGroupAddon>₽</InputGroupAddon>
|
||||||
|
<InputNumber v-model="amount" placeholder="Сумма"/>
|
||||||
|
<InputGroupAddon>.00</InputGroupAddon>
|
||||||
|
</InputGroup>
|
||||||
|
|
||||||
|
<!-- Кнопки -->
|
||||||
|
<div class="flex justify-content-end gap-2 mt-4">
|
||||||
|
<Button label="Сохранить" icon="pi pi-check" @click="savePayment" class="p-button-success"/>
|
||||||
|
<Button label="Отмена" icon="pi pi-times" @click="closeModal" class="p-button-secondary"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -3,12 +3,15 @@
|
|||||||
<div v-if="loading">
|
<div v-if="loading">
|
||||||
<LoadingView/>
|
<LoadingView/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex flex-col h-full bg-gray-100 py-15">
|
<div v-else class="flex flex-col h-full bg-gray-100 pb-5 gap-4">
|
||||||
<!-- Заголовок -->
|
<!-- Заголовок -->
|
||||||
<h1 class="text-4xl font-extrabold mb-8 text-gray-800">Ежемесячные платежи</h1>
|
<div class="flex flex-col gap-4 xl:flex-row justify-between bg-gray-100">
|
||||||
|
<h1 class="text-4xl text-gray-800">Ежемесячные платежи</h1>
|
||||||
|
<Button label="Добавить платеж" icon="pi pi-plus" class="text-sm " @click="toggleModal(null)"/>
|
||||||
|
</div>
|
||||||
<div v-if="!space">Выберите сперва пространство</div>
|
<div v-if="!space">Выберите сперва пространство</div>
|
||||||
<!-- Список рекуррентных платежей -->
|
<!-- Список рекуррентных платежей -->
|
||||||
<div v-else class="grid grid-cols-1 lg:grid-cols-3 gap-6 w-full max-w-7xl px-4">
|
<div v-else class="flex flex-col md:grid md:grid-cols-2 lg:grid-cols-2 gap-3 justify-between">
|
||||||
<RecurrentListItem
|
<RecurrentListItem
|
||||||
v-for="payment in recurrentPayments"
|
v-for="payment in recurrentPayments"
|
||||||
:key="payment.id"
|
:key="payment.id"
|
||||||
@@ -17,25 +20,24 @@
|
|||||||
@delete-payment="deletePayment"
|
@delete-payment="deletePayment"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<!-- <div-->
|
||||||
class="recurrent-card bg-white shadow-lg rounded-lg p-6 w-full transition duration-300 transform hover:scale-105">
|
<!-- class=" bg-white shadow-lg rounded-lg p-6 ">-->
|
||||||
<div class="flex justify-between items-center">
|
<!-- <div class="flex justify-between items-center">-->
|
||||||
<div class="flex items-center w-full justify-center gap-4" style="height: 160px;">
|
<!-- <div class="flex items-center w-full justify-center gap-4" style="height: 160px;">-->
|
||||||
|
|
||||||
<Button text class="flex-col" @click="toggleModal">
|
<!-- <Button text class="flex-col" @click="toggleModal">-->
|
||||||
<i class="pi pi-plus-circle" style="font-size: 2.5rem"></i>
|
<!-- <i class="pi pi-plus-circle" style="font-size: 2.5rem"></i>-->
|
||||||
<p>Add new</p>
|
<!-- <p></p>-->
|
||||||
</Button>
|
<!-- </Button>-->
|
||||||
<CreateRecurrentModal
|
<CreateRecurrentModal
|
||||||
|
v-if="showModal"
|
||||||
:show="showModal"
|
:show="showModal"
|
||||||
:expenseCategories="expenseCategories"
|
:expenseCategories="expenseCategories"
|
||||||
:incomeCategories="incomeCategories"
|
:incomeCategories="incomeCategories"
|
||||||
:categoryTypes="categoryTypes"
|
:categoryTypes="categoryTypes"
|
||||||
@save-payment="savePayment"
|
@save-payment="savePayment"
|
||||||
@close-modal="toggleModal"/>
|
@close-modal="toggleModal"/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,6 +72,7 @@ watch(
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
// Если выбранный space изменился, получаем новую информацию о бюджете
|
// Если выбранный space изменился, получаем новую информацию о бюджете
|
||||||
await fetchRecurrentPayments()
|
await fetchRecurrentPayments()
|
||||||
|
await fetchCategories()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching budget infos:', error);
|
console.error('Error fetching budget infos:', error);
|
||||||
}
|
}
|
||||||
@@ -143,11 +146,12 @@ const deletePayment = (payment: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (space.value){
|
if (space.value) {
|
||||||
await fetchRecurrentPayments()
|
await fetchRecurrentPayments()
|
||||||
|
await fetchCategories()
|
||||||
}
|
}
|
||||||
|
|
||||||
await fetchCategories()
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="payment.category.type.code == 'INCOME' ? 'from-green-100 to-green-50' : ' from-red-100 to-red-50' "
|
<div
|
||||||
class="recurrent-card bg-gradient-to-r shadow-xl rounded-lg p-6 w-full hover:shadow-2xl transition duration-300 transform hover:scale-105">
|
class="recurrent-card bg-gradient-to-r shadow-xl rounded-lg p-6 w-full ">
|
||||||
<div class="flex flex-col gap-5 justify-between items-start">
|
<div class="flex flex-col gap-5 justify-between items-start">
|
||||||
<!-- Дата и Сумма -->
|
<!-- Дата и Сумма -->
|
||||||
<div class="flex flex-row justify-between w-full items-center">
|
<div class="flex flex-row justify-between w-full items-center">
|
||||||
<div class="text-gray-500">
|
<div class="text-gray-500">
|
||||||
<strong class="text-3xl text-green-600">{{ payment.atDay }} </strong> числа
|
<strong class="text-xl text-green-600">{{ payment.atDay }} </strong> числа | {{payment.category.type.code=='EXPENSE' ? 'Расход' : 'Приход'}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<Button icon="pi pi-pencil" class="p-button-rounded p-button-text p-button-sm" @click="editPayment"/>
|
<button @click="editPayment"><i class="pi pi-pen-to-square"/></button>
|
||||||
<Button icon="pi pi-trash" class="p-button-rounded p-button-text p-button-danger p-button-sm"
|
<button @click="deletePayment"><i class="pi pi-trash"/></button>
|
||||||
@click="deletePayment"/>
|
<!-- <Button icon="pi pi-pencil" class="p-button-rounded p-button-text p-button-sm" @click="editPayment"/>-->
|
||||||
|
<!-- <Button icon="pi pi-trash" class="p-button-rounded p-button-text p-button-danger p-button-sm"-->
|
||||||
|
<!-- @click="deletePayment"/>-->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@@ -25,16 +27,16 @@
|
|||||||
<!-- Информация о платеже -->
|
<!-- Информация о платеже -->
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<div class="flex items-center ">
|
<div class="flex items-center ">
|
||||||
<span class="text-4xl">{{ payment.category.icon }}</span>
|
<span class="text-3xl">{{ payment.category.icon }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<h2 class="text-xl font-semibold text-gray-800">{{ payment.name }}</h2>
|
<h2 class="text-lg font-semibold text-gray-800">{{ payment.name }}</h2>
|
||||||
<p class="text-sm text-gray-500 line-clamp-1">{{ payment.description }}</p>
|
<p class="text-sm text-gray-500 line-clamp-1">{{ payment.description }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
:class="payment.category.type.code == 'EXPENSE' ? 'text-red-700' : 'text-green-700'"
|
:class="payment.category.type.code == 'EXPENSE' ? 'text-red-700' : 'text-green-700'"
|
||||||
class="text-2xl font-bold line-clamp-1 ">{{ formatAmount(payment.amount) }} ₽
|
class="text-xl font-bold line-clamp-1 ">{{ formatAmount(payment.amount) }} ₽
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -72,15 +74,7 @@ const deletePayment = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.recurrent-card {
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
//background: linear-gradient(135deg, #f0fff4, #c6f6d5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.recurrent-card:hover {
|
|
||||||
transform: scale(1.05);
|
|
||||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,75 +1,27 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {onMounted, ref} from "vue";
|
|
||||||
import {createSpaceRequest} from "@/services/spaceService";
|
|
||||||
import {Space} from "@/models/Space";
|
|
||||||
import Dialog from "primevue/dialog";
|
|
||||||
import FloatLabel from "primevue/floatlabel";
|
|
||||||
import Checkbox from "primevue/checkbox";
|
|
||||||
import InputText from "primevue/inputtext";
|
|
||||||
import Textarea from "primevue/textarea";
|
|
||||||
import Button from "primevue/button";
|
|
||||||
import DatePicker from "primevue/datepicker";
|
|
||||||
|
|
||||||
const emits = defineEmits(['space-created', 'close-modal', 'error-space-creation'])
|
import SpaceCreationFormView from "@/components/spaces/SpaceCreationFormView.vue";
|
||||||
|
import Dialog from "primevue/dialog";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
opened: {
|
opened: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
const emits = defineEmits(['space-created', 'close-modal', 'error-space-creation'])
|
||||||
const spaceName = ref('')
|
|
||||||
const spaceDescription = ref('')
|
|
||||||
const createCategories = ref(true)
|
|
||||||
|
|
||||||
const cancel = () => {
|
const cancel = () => {
|
||||||
resetForm()
|
// resetForm()
|
||||||
emits("close-modal");
|
emits("close-modal");
|
||||||
}
|
}
|
||||||
|
|
||||||
const createSpace = async () => {
|
|
||||||
const space = new Space()
|
|
||||||
space.name = spaceName.value
|
|
||||||
space.description = spaceDescription.value
|
|
||||||
space.createCategories = createCategories.value
|
|
||||||
try {
|
|
||||||
await createSpaceRequest(space)
|
|
||||||
resetForm()
|
|
||||||
emits("space-created")
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
emits('error-space-creation', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const resetForm = () => {
|
|
||||||
spaceName.value = ''
|
|
||||||
spaceDescription.value = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Dialog :visible="opened" modal header="Создать новое пространство" :style="{ width: '25rem' }" @hide="cancel"
|
<Dialog :visible="opened" modal header="Создать новое пространство" :style="{ width: '25rem' }" @hide="cancel"
|
||||||
@update:visible="cancel">
|
@update:visible="cancel">
|
||||||
|
|
||||||
<div class="flex flex-col gap-4 mt-1">
|
<SpaceCreationFormView @close-modal="cancel" @space-created="emits('space-created')" />
|
||||||
<FloatLabel variant="on" class="w-full">
|
|
||||||
<label for="name">Название</label>
|
|
||||||
<InputText v-model="spaceName" id="name" class="w-full"/>
|
|
||||||
</FloatLabel>
|
|
||||||
<FloatLabel variant="on" class="w-full">
|
|
||||||
<label for="name">Описание</label>
|
|
||||||
<Textarea v-model="spaceDescription" id="name" class="w-full"/>
|
|
||||||
</FloatLabel>
|
|
||||||
<div class="flex flex-row items-center min-w-fit gap-4">
|
|
||||||
<Checkbox v-model="createCategories" binary/>
|
|
||||||
Создать стандартный набор категорий?
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-row gap-2 justify-end items-center">
|
|
||||||
<Button label="Создать" severity="success" icon="pi pi-save" @click="createSpace"/>
|
|
||||||
<Button label="Отмена" severity="secondary" icon="pi pi-times-circle" @click="cancel"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
72
src/components/spaces/SpaceCreationFormView.vue
Normal file
72
src/components/spaces/SpaceCreationFormView.vue
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import {onMounted, ref} from "vue";
|
||||||
|
import {createSpaceRequest} from "@/services/spaceService";
|
||||||
|
import {Space} from "@/models/Space";
|
||||||
|
|
||||||
|
import FloatLabel from "primevue/floatlabel";
|
||||||
|
import Checkbox from "primevue/checkbox";
|
||||||
|
import InputText from "primevue/inputtext";
|
||||||
|
import Textarea from "primevue/textarea";
|
||||||
|
import Button from "primevue/button";
|
||||||
|
import DatePicker from "primevue/datepicker";
|
||||||
|
|
||||||
|
const emits = defineEmits(['space-created', 'close-modal', 'error-space-creation'])
|
||||||
|
|
||||||
|
|
||||||
|
const spaceName = ref('')
|
||||||
|
const spaceDescription = ref('')
|
||||||
|
const createCategories = ref(true)
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
resetForm()
|
||||||
|
emits("close-modal");
|
||||||
|
}
|
||||||
|
|
||||||
|
const createSpace = async () => {
|
||||||
|
const space = new Space()
|
||||||
|
space.name = spaceName.value
|
||||||
|
space.description = spaceDescription.value
|
||||||
|
space.createCategories = createCategories.value
|
||||||
|
try {
|
||||||
|
await createSpaceRequest(space).then((res) => {
|
||||||
|
resetForm()
|
||||||
|
emits("space-created", res)
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
emits('error-space-creation', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const resetForm = () => {
|
||||||
|
spaceName.value = ''
|
||||||
|
spaceDescription.value = ''
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-4 mt-1">
|
||||||
|
<FloatLabel variant="on" class="w-full">
|
||||||
|
<label for="name">Название</label>
|
||||||
|
<InputText v-model="spaceName" id="name" class="w-full"/>
|
||||||
|
</FloatLabel>
|
||||||
|
<FloatLabel variant="on" class="w-full">
|
||||||
|
<label for="name">Описание</label>
|
||||||
|
<Textarea v-model="spaceDescription" id="name" class="w-full"/>
|
||||||
|
</FloatLabel>
|
||||||
|
<div class="flex flex-row items-center min-w-fit gap-4">
|
||||||
|
<Checkbox v-model="createCategories" binary/>
|
||||||
|
Создать стандартный набор категорий?
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row gap-2 justify-end items-center">
|
||||||
|
<Button label="Создать" severity="success" icon="pi pi-save" @click="createSpace"/>
|
||||||
|
<Button label="Отмена" severity="secondary" icon="pi pi-times-circle" @click="cancel"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -227,7 +227,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<LoadingView v-if="loadingValue"/>
|
<LoadingView v-if="loadingValue"/>
|
||||||
|
|
||||||
<div v-else class="p-4 bg-gray-100 h-full grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-4 ">
|
<div v-else class="p-4 bg-gray-100 grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-4 ">
|
||||||
<Toast/>
|
<Toast/>
|
||||||
<ConfirmDialog/>
|
<ConfirmDialog/>
|
||||||
<Dialog :visible="inviteCreatedDialog" header="Приглашение" @hide="inviteCreatedDialog=false"
|
<Dialog :visible="inviteCreatedDialog" header="Приглашение" @hide="inviteCreatedDialog=false"
|
||||||
|
|||||||
@@ -83,10 +83,12 @@ const isReady = computed(() => !loading.value && loadingUser.value)
|
|||||||
// Получение категорий и типов транзакций
|
// Получение категорий и типов транзакций
|
||||||
const fetchCategoriesAndTypes = async () => {
|
const fetchCategoriesAndTypes = async () => {
|
||||||
try {
|
try {
|
||||||
const [categoriesResponse, categoryTypesResponse, transactionTypesResponse] = await Promise.all([
|
const [categoriesResponse, categoryTypesResponse, transactionTypesResponse, transactionsResponse] = await Promise.all([
|
||||||
getCategories(),
|
getCategories(),
|
||||||
getCategoryTypes(),
|
getCategoryTypes(),
|
||||||
getTransactionTypes()
|
getTransactionTypes(),
|
||||||
|
getTransactions()
|
||||||
|
|
||||||
]);
|
]);
|
||||||
entireCategories.value = categoriesResponse.data;
|
entireCategories.value = categoriesResponse.data;
|
||||||
expenseCategories.value = categoriesResponse.data.filter((category: Category) => category.type.code === 'EXPENSE');
|
expenseCategories.value = categoriesResponse.data.filter((category: Category) => category.type.code === 'EXPENSE');
|
||||||
@@ -94,6 +96,8 @@ const fetchCategoriesAndTypes = async () => {
|
|||||||
|
|
||||||
categoryTypes.value = categoryTypesResponse.data;
|
categoryTypes.value = categoryTypesResponse.data;
|
||||||
transactionTypes.value = transactionTypesResponse.data;
|
transactionTypes.value = transactionTypesResponse.data;
|
||||||
|
transactions.value = transactionsResponse.data;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.add({severity: 'error', summary: 'Ошибка!', detail: error.response.data["message"], life: 3000});
|
toast.add({severity: 'error', summary: 'Ошибка!', detail: error.response.data["message"], life: 3000});
|
||||||
console.error('Error fetching categories and types:', error);
|
console.error('Error fetching categories and types:', error);
|
||||||
@@ -125,8 +129,8 @@ const prepareData = () => {
|
|||||||
editedTransaction.value.type = transactionTypes.value.find(type => type.code === props.transactionType) || transactionTypes.value[0];
|
editedTransaction.value.type = transactionTypes.value.find(type => type.code === props.transactionType) || transactionTypes.value[0];
|
||||||
selectedCategoryType.value = categoryTypes.value.find(type => type.code === props.categoryType) || categoryTypes.value[0];
|
selectedCategoryType.value = categoryTypes.value.find(type => type.code === props.categoryType) || categoryTypes.value[0];
|
||||||
|
|
||||||
entireCategories.value.find(category => category.id == props.categoryId ) ? entireCategories.value.find(category => category.id == props.categoryId ) : props.categoryType === 'EXPENSE' ? expenseCategories.value[0] : incomeCategories.value[0]
|
entireCategories.value.find(category => category.id == props.categoryId) ? entireCategories.value.find(category => category.id == props.categoryId) : props.categoryType === 'EXPENSE' ? expenseCategories.value[0] : incomeCategories.value[0]
|
||||||
editedTransaction.value.category = entireCategories.value.find(category => category.id == props.categoryId ) ? entireCategories.value.find(category => category.id == props.categoryId ) : props.categoryType === 'EXPENSE' ? expenseCategories.value[0] : incomeCategories.value[0]
|
editedTransaction.value.category = entireCategories.value.find(category => category.id == props.categoryId) ? entireCategories.value.find(category => category.id == props.categoryId) : props.categoryType === 'EXPENSE' ? expenseCategories.value[0] : incomeCategories.value[0]
|
||||||
// editedTransaction.value.category = props.categoryType === 'EXPENSE' ? expenseCategories.value[0] : incomeCategories.value[0];
|
// editedTransaction.value.category = props.categoryType === 'EXPENSE' ? expenseCategories.value[0] : incomeCategories.value[0];
|
||||||
editedTransaction.value.date = new Date();
|
editedTransaction.value.date = new Date();
|
||||||
} else {
|
} else {
|
||||||
@@ -163,6 +167,9 @@ const showError = (message) => {
|
|||||||
result.value = true;
|
result.value = true;
|
||||||
isError.value = true;
|
isError.value = true;
|
||||||
resultText.value = message;
|
resultText.value = message;
|
||||||
|
setTimeout(() => {
|
||||||
|
result.value = false
|
||||||
|
}, 1000)
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -226,7 +233,7 @@ const createTransaction = async (): Promise<void> => {
|
|||||||
|
|
||||||
|
|
||||||
const transactionsUpdatedEmit = async () => {
|
const transactionsUpdatedEmit = async () => {
|
||||||
await getTransactions(spaceStore.space?.id, 'INSTANT', 'EXPENSE', null, user.value.id, false, 3).then(transactionsResponse => transactions.value = transactionsResponse.data);
|
await getTransactions( 'INSTANT', 'EXPENSE', null, user.value.id, false, 3).then(transactionsResponse => transactions.value = transactionsResponse.data);
|
||||||
|
|
||||||
EventBus.emit('transactions-updated', true)
|
EventBus.emit('transactions-updated', true)
|
||||||
}
|
}
|
||||||
@@ -310,7 +317,7 @@ const transactions = ref<Transaction[]>(null);
|
|||||||
const spaceStore = useSpaceStore()
|
const spaceStore = useSpaceStore()
|
||||||
const selectedSpace = computed(() => spaceStore.space)
|
const selectedSpace = computed(() => spaceStore.space)
|
||||||
|
|
||||||
watch( selectedSpace, async (newValue, oldValue) => {
|
watch(selectedSpace, async (newValue, oldValue) => {
|
||||||
if (newValue != oldValue) {
|
if (newValue != oldValue) {
|
||||||
if (!isEditing.value) {
|
if (!isEditing.value) {
|
||||||
|
|
||||||
@@ -333,7 +340,6 @@ onMounted(async () => {
|
|||||||
if (!isEditing.value) {
|
if (!isEditing.value) {
|
||||||
|
|
||||||
// transactions.value = transactions.value.slice(0,3)
|
// transactions.value = transactions.value.slice(0,3)
|
||||||
await getTransactions(selectedSpace.value?.id,'INSTANT', 'EXPENSE', null, user.value.id, false, 3).then(transactionsResponse => transactions.value = transactionsResponse.data);
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -369,7 +375,7 @@ onMounted(async () => {
|
|||||||
<LoadingView v-if="loading"/>
|
<LoadingView v-if="loading"/>
|
||||||
<div v-else class=" grid gap-4 w-full ">
|
<div v-else class=" grid gap-4 w-full ">
|
||||||
<div class="relative w-full justify-center justify-items-center ">
|
<div class="relative w-full justify-center justify-items-center ">
|
||||||
<div class="flex flex-col justify-items-center gap-2">
|
<div class="flex flex-col justify-items-center gap-2 w-full">
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<Select v-if="!isEditing" v-model="editedTransaction.type" :allow-empty="false"
|
<Select v-if="!isEditing" v-model="editedTransaction.type" :allow-empty="false"
|
||||||
:options="transactionTypes"
|
:options="transactionTypes"
|
||||||
|
|||||||
@@ -28,8 +28,7 @@ const fetchTransactions = async (reload) => {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const response = await getTransactions( 'INSTANT', null, null, selectedUserId.value ? selectedUserId.value : null, null, reload ? offset.value : limit, reload ? 0 : offset.value);
|
||||||
const response = await getTransactions(selectedSpace.value?.id, 'INSTANT', null, null, selectedUserId.value ? selectedUserId.value : null, null, reload ? offset.value : limit, reload ? 0 : offset.value);
|
|
||||||
const newTransactions = response.data;
|
const newTransactions = response.data;
|
||||||
|
|
||||||
// Проверка на конец данных
|
// Проверка на конец данных
|
||||||
@@ -115,14 +114,14 @@ const selectedSpace = computed(() => spaceStore.space)
|
|||||||
|
|
||||||
watch( selectedSpace, async (newValue, oldValue) => {
|
watch( selectedSpace, async (newValue, oldValue) => {
|
||||||
if (newValue != oldValue) {
|
if (newValue != oldValue) {
|
||||||
await fetchTransactions(true)
|
await fetchTransactions(false)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const types = ref([])
|
const types = ref([])
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
EventBus.on('transactions-updated', fetchTransactions,true);
|
EventBus.on('transactions-updated', fetchTransactions,true);
|
||||||
if (selectedSpace.value){
|
if (selectedSpace.value){
|
||||||
await fetchTransactions(); // Первоначальная загрузка данных
|
await fetchTransactions(false); // Первоначальная загрузка данных
|
||||||
|
|
||||||
}
|
}
|
||||||
// await fetchUsers();
|
// await fetchUsers();
|
||||||
|
|||||||
@@ -94,8 +94,9 @@ export const updateBudgetCategoryRequest = async (budget_id, category: BudgetCat
|
|||||||
|
|
||||||
export const createBudget = async (budget: Budget, createRecurrent: Boolean) => {
|
export const createBudget = async (budget: Budget, createRecurrent: Boolean) => {
|
||||||
|
|
||||||
try {
|
|
||||||
let spaceId = localStorage.getItem("spaceId")
|
const spaceStore = useSpaceStore()
|
||||||
|
console.log(budget)
|
||||||
let budgetToCreate = JSON.parse(JSON.stringify(budget));
|
let budgetToCreate = JSON.parse(JSON.stringify(budget));
|
||||||
budgetToCreate.dateFrom = format(budget.dateFrom, 'yyyy-MM-dd')
|
budgetToCreate.dateFrom = format(budget.dateFrom, 'yyyy-MM-dd')
|
||||||
budgetToCreate.dateTo = format(budget.dateTo, 'yyyy-MM-dd')
|
budgetToCreate.dateTo = format(budget.dateTo, 'yyyy-MM-dd')
|
||||||
@@ -103,12 +104,12 @@ export const createBudget = async (budget: Budget, createRecurrent: Boolean) =>
|
|||||||
budget: budgetToCreate,
|
budget: budgetToCreate,
|
||||||
createRecurrent: createRecurrent
|
createRecurrent: createRecurrent
|
||||||
}
|
}
|
||||||
await apiClient.post(`/spaces/${spaceId}/budgets`, data);
|
return await apiClient.post(`/spaces/${spaceStore.space?.id}/budgets`, budgetToCreate)
|
||||||
|
.then(res => res.data)
|
||||||
|
.catch(err => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
|
||||||
} catch (e){
|
|
||||||
console.error(e)
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -38,9 +38,11 @@ export const editCategoryRequest = async (category: any) => {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteCategory = async (id: number) => {
|
export const deleteCategoryRequest = async (id: string) => {
|
||||||
const spaceStore = useSpaceStore();
|
const spaceStore = useSpaceStore();
|
||||||
return await apiClient.delete(`/spaces/${spaceStore.space?.id}/categories/${id}`);
|
return await apiClient.delete(`/spaces/${spaceStore.space?.id}/categories/${id}`)
|
||||||
|
.then(res => res.data)
|
||||||
|
.catch(err => {throw err})
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCategoriesSumsRequest = async (spaceId: string) => {
|
export const getCategoriesSumsRequest = async (spaceId: string) => {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const getTransaction = async (transactionId: int) => {
|
|||||||
return await apiClient.post(`/transactions/${transactionId}`,);
|
return await apiClient.post(`/transactions/${transactionId}`,);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTransactions = async (spaceId, transaction_type = null, category_type = null, category_id = null, user_id = null, is_child = null, limit = null, offset = null) => {
|
export const getTransactions = async ( transaction_type = null, category_type = null, category_id = null, user_id = null, is_child = null, limit = null, offset = null) => {
|
||||||
const params = {};
|
const params = {};
|
||||||
// params.spaceId=spaceId;
|
// params.spaceId=spaceId;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user