tags and new analytics new in budget

This commit is contained in:
xds
2025-02-23 12:22:40 +03:00
parent 6d3638896f
commit 85b6d0a796
29 changed files with 1102 additions and 565 deletions

View File

@@ -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>

View File

@@ -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;

View File

@@ -18,6 +18,11 @@
height: 0.5rem !important; height: 0.5rem !important;
} }
.p-selectbutton{
border-width: 1px !important;
border-color: #d1d5db !important;
}
canvas { canvas {

View File

@@ -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: () => {

View File

@@ -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']);

View 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>

View File

@@ -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)
@@ -75,7 +96,7 @@ watch(
try { try {
// loading.value = true; // loading.value = true;
// Если выбранный space изменился, получаем новую информацию о бюджете // Если выбранный space изменился, получаем новую информацию о бюджете
await fetchCategories() await fetchCategories()
} catch (error) { } catch (error) {
console.error('Error fetching budget infos:', error); console.error('Error fetching budget infos:', error);
} }
@@ -90,38 +111,34 @@ 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> <InputText v-model="budget.name" id="name" class="w-full"/>
<InputText v-model="budget.name" id="name" class="w-full"/> </FloatLabel>
<div class="flex flex-row gap-4">
<FloatLabel variant="on">
<label for="dateFrom">Дата начала</label>
<DatePicker v-model="budget.dateFrom" id="dateFrom" dateFormat="dd.mm.yy"/>
</FloatLabel>
<FloatLabel variant="on">
<label for="dateTo">Дата завершения</label>
<DatePicker v-model="budget.dateTo" id="dateTo" dateFormat="dd.mm.yy"/>
</FloatLabel> </FloatLabel>
<div class="flex flex-row gap-4">
<FloatLabel variant="on">
<label for="dateFrom">Дата начала</label>
<DatePicker v-model="budget.dateFrom" id="dateFrom" dateFormat="dd.mm.yy"/>
</FloatLabel>
<FloatLabel variant="on">
<label for="dateTo">Дата завершения</label>
<DatePicker v-model="budget.dateTo" id="dateTo" dateFormat="dd.mm.yy"/>
</FloatLabel>
</div>
<div class="flex flex-row items-center min-w-fit gap-4">
<Checkbox v-model="createRecurrentPayments" binary/>
Создать ежемесячные платежи?
</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">
<Button label="Создать" severity="success" icon="pi pi-save" @click="create"/>
<Button label="Отмена" severity="secondary" icon="pi pi-times-circle" @click="cancel"/>
</div>
</div> </div>
</Dialog> <!-- <div class="flex flex-row items-center min-w-fit gap-4">-->
<!-- <Checkbox v-model="createRecurrentPayments" binary/>-->
<!-- Создать ежемесячные платежи?-->
<!-- </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">
<Button label="Создать" severity="success" icon="pi pi-save" @click="create"/>
<Button label="Отмена" severity="secondary" icon="pi pi-times-circle" @click="cancel"/>
</div>
</div>
</template> </template>
<style scoped> <style scoped>

View File

@@ -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) => {

View File

@@ -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
}} | }} |

View File

@@ -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,153 +1145,166 @@ onUnmounted(async () => {
</li> </li>
</ul> </ul>
</div> </div>
</div>
<div>
<!-- Планируемые расходы -->
<div class="pb-4">
<div class="flex flex-row gap-4 items-center mb-4"> <div>
<h3 class="text-2xl font-bold text-rose-500 ">Расходы</h3> <!-- Планируемые расходы -->
<button @click="openDrawer('PLANNED', 'EXPENSE')"> <div class="flex flex-col gap-1">
<span class="font-light text-sm">+ Добавить</span>
</button>
</div>
<div class="flex flex-col gap-4 pb-4">
<div class="flex flex-row gap-2"><span class="text-lg font-bold items-center">Календарь</span>
<button class="font-light text-sm" @click="toggleCalendar">
{{ calendarExpanded ? 'Скрыть' : 'Раскрыть' }}
<div class="flex flex-row gap-0 items-center ">
<h3 class="text-2xl font-bold text-rose-500 ">Расходы</h3>
<button @click="openDrawer('PLANNED', 'EXPENSE')">
<span class="font-light text-sm">+ Добавить</span>
</button> </button>
</div> </div>
<div class="flex flex-col gap-1 ">
<div class="flex flex-row gap-2"><span class="text-lg font-bold items-center">Календарь</span>
<button class="font-light text-sm" @click="toggleCalendar">
{{ calendarExpanded ? 'Скрыть' : 'Раскрыть' }}
<div v-for="day, dayOne in calendar" class="flex flex-col justify-between p-4 shadow-md rounded-lg " </button>
v-if="calendarExpanded == '1'"
:class="day.date.toISOString().split('T')[0] == new Date().toISOString().split('T')[0]? 'bg-emerald-200' : 'bg-white '">
<div class="flex flex-row gap-2 items-center ">
<span class="font-bold text-xl">{{ day.dateStr }} </span>
<span
v-if="day.expensesSum>0">Трат по плану: {{ formatAmount(day.expensesSum) }} </span>
</div> </div>
<BudgetTransactionView v-for="expense in day.expenses" :key="expense.id" <div v-for="day, dayOne in calendar" class="flex flex-col justify-between p-4 shadow-md rounded-lg "
:transaction="expense" v-if="calendarExpanded == '1'"
:is-list="false" :class="day.date.toISOString().split('T')[0] == new Date().toISOString().split('T')[0]? 'bg-emerald-200' : 'bg-white '">
@transaction-updated="updateTransactions" <div class="flex flex-row gap-2 items-center ">
@transaction-checked="updateTransactions" <span class="font-bold text-xl">{{ day.dateStr }} </span>
/> <span
v-if="day.expensesSum>0">Трат по плану: {{ formatAmount(day.expensesSum) }} </span>
</div>
<BudgetTransactionView v-for="expense in day.expenses" :key="expense.id"
:transaction="expense"
:is-list="false"
@transaction-updated="updateTransactions"
@transaction-checked="updateTransactions"
/>
</div>
</div> </div>
</div> <ul class="grid grid-cols-1 md:grid-cols-2 gap-2">
<ul class="grid grid-cols-1 md:grid-cols-2 gap-4"> <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=""> <div class="flex flex-row justify-between w-full items-center">
<div class="flex flex-row justify-between w-full items-center"> <div class="flex flex-row col-span-2 gap-2 items-center">
<div class="flex flex-row col-span-2 gap-2 items-center">
<span class=" font-bold line-clamp-1" style="font-size: 1.25rem">{{ <span class=" font-bold line-clamp-1" style="font-size: 1.25rem">{{
category.name.category.icon category.name.category.icon
}} {{ category.name.category.name }}</span> }} {{ category.name.category.name }}</span>
<button @click="openDrawer('INSTANT', 'EXPENSE', category.name.category.id)"> <button @click="openDrawer('INSTANT', 'EXPENSE', category.name.category.id)">
<i class="pi pi-plus-circle"/> <i class="pi pi-plus-circle"/>
</button> </button>
</div> </div>
<div class="flex flex-row w-fit gap-1 justify-between"> <div class="flex flex-row w-fit gap-1 justify-between">
<span>{{ formatAmount(category.name.currentSpent) }} </span> <span>{{ formatAmount(category.name.currentSpent) }} </span>
<span> / </span> <span> / </span>
<!-- Если редактируемая категория показываем input --> <!-- Если редактируемая категория показываем input -->
<input <input
v-if="editingCategoryId === category.name.category.id" v-if="editingCategoryId === category.name.category.id"
v-model.number="editableLimit[category.name.category.id]" v-model.number="editableLimit[category.name.category.id]"
@blur="(category)" @blur="(category)"
@keyup.enter="updateBudgetCategory(category.name, editableLimit[category.name.category.id])" @keyup.enter="updateBudgetCategory(category.name, editableLimit[category.name.category.id])"
type="number" type="number"
class=" border-b w-20 p-0 " class=" border-b w-20 p-0 "
autofocus autofocus
/> />
<!-- Если НЕ редактируем, показываем текст --> <!-- Если НЕ редактируем, показываем текст -->
<span v-else @click="startEditing(category)" <span v-else @click="startEditing(category)"
class=" p-0 cursor-pointer hover:underline text-left"> {{ class=" p-0 cursor-pointer hover:underline text-left"> {{
formatAmount(category.name.currentLimit) formatAmount(category.name.currentLimit)
}} </span> }} </span>
<button v-if="category.transactions.length>0" <button v-if="category.transactions.length>0"
:class="{'rotate-90': expenseCategoriesState[categoryId].isOpened}" :class="{'rotate-90': expenseCategoriesState[categoryId].isOpened}"
@click="expenseCategoriesState[categoryId].isOpened = !expenseCategoriesState[categoryId].isOpened"> @click="expenseCategoriesState[categoryId].isOpened = !expenseCategoriesState[categoryId].isOpened">
<i class="pi pi-angle-right transition-transform duration-300 text-5xl"></i> <i class="pi pi-angle-right transition-transform duration-300 text-5xl"></i>
</button> </button>
</div>
</div>
<transition
enter-active-class="transition ease-out duration-300"
enter-from-class="opacity-0 transform -translate-y-2"
enter-to-class="opacity-100 transform translate-y-0"
leave-active-class="transition ease-in duration-300"
leave-from-class="opacity-100 transform translate-y-0"
leave-to-class="opacity-0 transform -translate-y-2"
>
<div v-if="expenseCategoriesState[categoryId].isOpened" class="pt-2 pl-2">
<BudgetTransactionView
v-for="transaction in category.transactions"
:key="transaction._id"
:transaction="transaction"
:is-list="false"
@transaction-updated="updateTransactions"
@transaction-checked="updateTransactions"
/>
<div
class="flex justify-between bg-white min-w-fit max-h-fit flex-row items-center gap-4 w-full ">
<div>
<p class=" font-bold text-gray-700 dark:text-gray-400">
🗓 Остаток на внеплановые
</p>
</div>
<div class="text-lg line-clamp-1 ">
{{
formatAmount(category.name.currentLimit - category.name.currentPlanned)
}}
</div>
</div> </div>
</div> </div>
</transition> <transition
</div> enter-active-class="transition ease-out duration-300"
<div class="flex flex-col gap-0"> enter-from-class="opacity-0 transform -translate-y-2"
<div class="flex flex-row justify-between w-full items-center"> enter-to-class="opacity-100 transform translate-y-0"
<span class="font-bold">🗓 Остаток всего:</span> leave-active-class="transition ease-in duration-300"
<span class="font-bold text-lg">{{ leave-from-class="opacity-100 transform translate-y-0"
formatAmount(category.name.currentLimit - category.name.currentSpent) leave-to-class="opacity-0 transform -translate-y-2"
>
<div v-if="expenseCategoriesState[categoryId].isOpened" class="pt-2 pl-2">
<BudgetTransactionView
v-for="transaction in category.transactions"
:key="transaction._id"
:transaction="transaction"
:is-list="false"
@transaction-updated="updateTransactions"
@transaction-checked="updateTransactions"
/>
<!-- <div-->
<!-- class="flex justify-between bg-white min-w-fit max-h-fit flex-row items-center gap-4 w-full ">-->
<!-- <div>-->
<!-- <p class=" font-bold text-gray-700 dark:text-gray-400">-->
<!-- 🗓 Остаток на внеплановые-->
<!-- </p>-->
<!-- </div>-->
<!-- <div class="text-lg line-clamp-1 ">-->
<!-- {{-->
<!-- formatAmount(category.name.currentLimit - category.name.currentPlanned)-->
<!-- }} -->
<!-- </div>-->
<!-- </div>-->
</div>
</transition>
</div>
<div class="flex flex-col gap-0">
<div class="flex flex-row gap-1 w-full items-center">
<span class="font-bold">{{
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>
</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> }} </span>
</div> </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 class="flex flex-row ">
<h3 class="text-2xl font-bold">Транзакции</h3>
<button @click="openDrawer('INSTANT', 'EXPENSE')">
<span class="font-light text-sm">+ Добавить</span>
</button>
</div>
<div class=" flex gap-2 overflow-x-auto">
<button v-for="categorySum in transactionCategoriesSums"
@click="selectCategoryType(categorySum.category.id)"
class="rounded-xl border p-1 bg-white border-gray-300 mb-2 min-w-fit px-2"
:class="selectedCategoryId == categorySum.category.id ? '!bg-blue-100' : ''">
<p><span class="text-sm font-bold">{{ categorySum.category.name }}</span>: {{ categorySum.sum }} </p>
</button> <div v-else class=" h-fit gap-4 flex-col row-span-6 lg:hidden ">
</div> <div class="flex flex-row ">
<h3 class="text-2xl font-bold">Транзакции</h3>
<button @click="openDrawer('INSTANT', 'EXPENSE')">
<span class="font-light text-sm">+ Добавить</span>
</button>
</div>
<div class=" flex gap-2 overflow-x-auto">
<button v-for="categorySum in transactionCategoriesSums"
@click="selectCategoryType(categorySum.category.id)"
class="rounded-xl border p-1 bg-white border-gray-300 mb-2 min-w-fit px-2"
:class="selectedCategoryId == categorySum.category.id ? '!bg-blue-100' : ''">
<p><span class="text-sm font-bold">{{ categorySum.category.name }}</span>: {{ categorySum.sum }} </p>
<div class="grid grid-cols-1 gap-1 max-h-tlist overflow-y-auto "> </button>
<BudgetTransactionView v-for="transaction in filteredTransactions" :key="transaction.id" </div>
:transaction="transaction"
:is-list="true" <div class="grid grid-cols-1 gap-1 max-h-tlist overflow-y-auto ">
@transaction-updated="updateTransactions" <BudgetTransactionView v-for="transaction in filteredTransactions" :key="transaction.id"
:transaction="transaction"
:is-list="true"
@transaction-updated="updateTransactions"
/> />
</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>

View File

@@ -36,7 +36,7 @@ onMounted(async () => {
<div v-else class=""> <div v-else class="">
<div class="flex flex-col bg-gray-200 outline outline-2 outline-gray-300 rounded-2xl p-4"> <div class="flex flex-col bg-gray-200 outline outline-2 outline-gray-300 rounded-2xl p-4">
<div class="flex flex-row items-center min-w-fit justify-between"> <div class="flex flex-row items-center min-w-fit justify-between">
<p class="text-2xl font-bold">Категории</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>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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();
await fetchCategoryTypes(); console.log("here");
} await fetchCategoryTypes();
loading.value = false; }
}) loading.value = false;
})
</script> </script>

View 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>

View File

@@ -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,26 +24,59 @@ 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
<div class="flex flex-row items-center p-x-4 gap-4"> 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">
<p class="text-6xl font-bold text-gray-700 dark:text-gray-400">{{ category.icon }}</p> <div class="flex items-center justify-between w-full">
<div class="flex flex-col items-start justify-items-start w-full"> <div class="flex flex-row items-center p-x-4 gap-4">
<p class="font-bold">{{ category.name }}</p> <p class="text-6xl font-bold text-gray-700 dark:text-gray-400">{{ category.icon }}</p>
<p class="font-light line-clamp-1">{{ category.description }}</p> <div class="flex flex-col items-start justify-items-start w-full">
<p class="font-bold">{{ category.name }}</p>
<p class="font-light line-clamp-1">{{ category.description }}</p>
</div>
</div>
<div class="flex flex-row items-center px-4 gap-2 ">
<button @click="openEdit"><i class="pi pi-pen-to-square"/></button>
<button @click="confirmDelete"><i class="pi pi-trash"/></button>
<!-- <Button icon="pi pi-trash" severity="danger" rounded @click="deleteCategory"/>-->
</div> </div>
</div> </div>
<div class="flex flex-row items-start w-full justify-start p-x-4 gap-2 ">
<div class="flex flex-row items-center p-x-4 gap-2 "> <Tag v-if="category.tags.length>0" v-for="tag in category.tags">{{ tag.name }} ({{ tag.code }})</Tag>
<Button icon="pi pi-pen-to-square" rounded @click="openEdit"/> <span class="p-1">&nbsp;</span>
<!-- <Button icon="pi pi-trash" severity="danger" rounded @click="deleteCategory"/>-->
</div> </div>
</div> </div>
</template> </template>

View File

@@ -1,50 +1,56 @@
<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 justify-center gap-4"> <div class="flex flex-col gap-2">
<Button rounded outlined @click="toggleEmojiPicker" class=" flex justify-center !rounded-full !text-4xl !p-4" <div class="flex justify-center gap-4">
size="large" :label="category.icon"/> <Button rounded outlined @click="toggleEmojiPicker" class=" flex justify-center !rounded-full !text-4xl !p-4"
</div> size="large" :label="category.icon"/>
<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>
<!-- Поля для создания/редактирования категории -->
<label for="newCategoryName">Название категории:</label>
<input v-model="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>
<div class="flex flex-col flex-wrap gap-2"> <!-- {{ // category }}-->
<h2 class="text-lg ">Доступные теги</h2> <!-- {{props.categoryTypes}}-->
<div class="flex flex-row flex-wrap gap-2"> <!-- {{categoryType}}-->
<Tag v-for="tag in availableTags" :key="tag.id" class="w-fit " @click="addTag(tag)"> <div
{{ tag.name }} ({{ tag.code }}) 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"
</Tag> 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-model="category.name" type="text" id="newCategoryName"/>
<label for="newCategoryDesc">Описание категории:</label>
<input v-model="category.description" type="text" id="newCategoryDesc"/>
<div v-if="showTags" 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 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>
<!-- Кнопки -->
<div class="button-group">
<button @click="saveCategory" class="create-category-btn">{{ isEditing ? 'Сохранить' : 'Создать' }}</button>
<button @click="closeModal" class="close-modal-btn">Отмена</button>
</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>
</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',

View File

@@ -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) => {

View 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);
// Список дней (131)
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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View 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>

View File

@@ -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"

View File

@@ -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;
}; };
@@ -176,7 +183,7 @@ const createTransaction = async (): Promise<void> => {
if (editedTransaction.value.type.code === 'INSTANT') { if (editedTransaction.value.type.code === 'INSTANT') {
editedTransaction.value.isDone = true; editedTransaction.value.isDone = true;
} }
loading.value = true; loading.value = true;
// Отправляем запрос на создание транзакции и ждём результата // Отправляем запрос на создание транзакции и ждём результата
const result = await createTransactionRequest(editedTransaction.value); const result = await createTransactionRequest(editedTransaction.value);
@@ -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)
} }
@@ -307,10 +314,10 @@ const isMobile = ref(false);
const userAgent = ref(null); const userAgent = ref(null);
const transactions = ref<Transaction[]>(null); 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"

View File

@@ -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();

View File

@@ -7,24 +7,24 @@ import {useSpaceStore} from "@/stores/spaceStore";
export const getBudgetInfos = async () => { export const getBudgetInfos = async () => {
try { try {
let spaceId = localStorage.getItem("spaceId") let spaceId = localStorage.getItem("spaceId")
let response = await apiClient.get(`/spaces/${spaceId}/budgets`); let response = await apiClient.get(`/spaces/${spaceId}/budgets`);
let budgetInfos = response.data; let budgetInfos = response.data;
budgetInfos.forEach((budgetInfo: Budget) => { budgetInfos.forEach((budgetInfo: Budget) => {
budgetInfo.dateFrom = new Date(budgetInfo.dateFrom); budgetInfo.dateFrom = new Date(budgetInfo.dateFrom);
budgetInfo.dateTo = new Date(budgetInfo.dateTo); budgetInfo.dateTo = new Date(budgetInfo.dateTo);
budgetInfo.plannedExpenses?.forEach(e => { budgetInfo.plannedExpenses?.forEach(e => {
e.date = new Date(e.date) e.date = new Date(e.date)
}) })
budgetInfo.plannedIncomes?.forEach(e => { budgetInfo.plannedIncomes?.forEach(e => {
e.date = new Date(e.date) e.date = new Date(e.date)
}) })
budgetInfo.transactions?.forEach(e => { budgetInfo.transactions?.forEach(e => {
e.date = new Date(e.date) e.date = new Date(e.date)
})
}) })
}) return budgetInfos
return budgetInfos
} catch (e) { } catch (e) {
console.log(e) console.log(e)
throw e throw e
@@ -94,21 +94,22 @@ 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")
let budgetToCreate = JSON.parse(JSON.stringify(budget));
budgetToCreate.dateFrom = format(budget.dateFrom, 'yyyy-MM-dd')
budgetToCreate.dateTo = format(budget.dateTo, 'yyyy-MM-dd')
let data = {
budget: budgetToCreate,
createRecurrent: createRecurrent
}
await apiClient.post(`/spaces/${spaceId}/budgets`, data);
} catch (e){ const spaceStore = useSpaceStore()
console.error(e) console.log(budget)
throw e let budgetToCreate = JSON.parse(JSON.stringify(budget));
} budgetToCreate.dateFrom = format(budget.dateFrom, 'yyyy-MM-dd')
budgetToCreate.dateTo = format(budget.dateTo, 'yyyy-MM-dd')
let data = {
budget: budgetToCreate,
createRecurrent: createRecurrent
}
return await apiClient.post(`/spaces/${spaceStore.space?.id}/budgets`, budgetToCreate)
.then(res => res.data)
.catch(err => {
throw err
})
} }

View File

@@ -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) => {

View File

@@ -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;