diff --git a/package-lock.json b/package-lock.json index 9a92c93cfaa7aaf8a2dc9f1f42c57fb14a6cb6fb..0c20a4e80edf18722c3f5638926a2d348f1a1517 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "axios": "^1.6.8", "bootstrap": "^5.3.3", "install": "^0.13.0", + "jQuery": "^1.7.4", "js-cookie": "^3.0.5", "oh-vue-icons": "^1.0.0-rc3", "openapi-typescript-codegen": "^0.29.0", @@ -6261,6 +6262,15 @@ "@sideway/pinpoint": "^2.0.0" } }, + "node_modules/jQuery": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/jQuery/-/jQuery-1.7.4.tgz", + "integrity": "sha512-hf/hWBnkFhu4FzP96tBjZNPF9qTcUaOKFA8hpVJX0Cb/892JefRzoVGCe/vkmry/pOhZiK6VnQvnuV8CoHf1rA==", + "deprecated": "This is deprecated. Please use 'jquery' (all lowercase).", + "engines": { + "node": ">=0.6" + } + }, "node_modules/js-beautify": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz", diff --git a/package.json b/package.json index 880d6c753b01bf3945cca11ac9c89dbfaab971bb..7451ccd606573141e0942ce9e76b9b6dfca81901 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "axios": "^1.6.8", "bootstrap": "^5.3.3", "install": "^0.13.0", + "jQuery": "^1.7.4", "js-cookie": "^3.0.5", "oh-vue-icons": "^1.0.0-rc3", "openapi-typescript-codegen": "^0.29.0", diff --git a/spec.json b/spec.json index 077195b6cedb85e2cc6e2ab7e8208027bb3a8c37..ec255e673f3a4bc77e022576875626abde8758f4 100644 --- a/spec.json +++ b/spec.json @@ -36,8 +36,8 @@ "required": true }, "responses": { - "404": { - "description": "Bank profile id does not exist", + "200": { + "description": "No accounts associated with a bank user", "content": { "*/*": { "schema": { @@ -46,8 +46,8 @@ } } }, - "200": { - "description": "No accounts associated with a bank user", + "404": { + "description": "Bank profile id does not exist", "content": { "*/*": { "schema": { @@ -78,8 +78,8 @@ "required": true }, "responses": { - "400": { - "description": "Could not create profile", + "200": { + "description": "Successfully created a bank profile", "content": { "*/*": { "schema": { @@ -88,8 +88,8 @@ } } }, - "200": { - "description": "Successfully created a bank profile", + "400": { + "description": "Could not create profile", "content": { "*/*": { "schema": { @@ -120,8 +120,8 @@ "required": true }, "responses": { - "404": { - "description": "Provided bank profile id could not be found", + "200": { + "description": "Successfully created account", "content": { "*/*": { "schema": { @@ -130,8 +130,8 @@ } } }, - "200": { - "description": "Successfully created account", + "404": { + "description": "Provided bank profile id could not be found", "content": { "*/*": { "schema": { @@ -188,17 +188,17 @@ "required": true }, "responses": { - "403": { - "description": "Invalid token" - }, "204": { "description": "Password was reset successfully" + }, + "403": { + "description": "Invalid token" } }, "security": [] } }, - "/api/images": { + "/api/image/upload": { "post": { "tags": [ "Image" @@ -208,7 +208,7 @@ "operationId": "uploadImage", "requestBody": { "content": { - "multipart/form-data": { + "application/json": { "schema": { "required": [ "file" @@ -225,7 +225,7 @@ } }, "responses": { - "201": { + "200": { "description": "Successfully uploaded the image", "content": { "*/*": { @@ -235,6 +235,17 @@ } } } + }, + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "type": "integer", + "format": "int64" + } + } + } } } } @@ -299,8 +310,8 @@ "required": true }, "responses": { - "500": { - "description": "Budget is not found", + "200": { + "description": "Successfully updated budget", "content": { "application/json": { "schema": { @@ -309,8 +320,8 @@ } } }, - "200": { - "description": "Successfully updated budget", + "500": { + "description": "Budget is not found", "content": { "application/json": { "schema": { @@ -352,8 +363,8 @@ "required": true }, "responses": { - "500": { - "description": "Error updating expense", + "200": { + "description": "Successfully updated budget", "content": { "application/json": { "schema": { @@ -362,8 +373,8 @@ } } }, - "200": { - "description": "Successfully updated budget", + "500": { + "description": "Error updating expense", "content": { "application/json": { "schema": { @@ -426,22 +437,22 @@ } ], "responses": { - "409": { - "description": "Email already exists", + "200": { + "description": "Email is valid", "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/ExceptionResponse" + "type": "object" } } } }, - "200": { - "description": "Email is valid", + "409": { + "description": "Email already exists", "content": { "*/*": { "schema": { - "type": "object" + "$ref": "#/components/schemas/ExceptionResponse" } } } @@ -610,38 +621,6 @@ } } }, - "/api/users/password": { - "patch": { - "tags": [ - "User" - ], - "summary": "Update a password", - "description": "Update the password of the authenticated user", - "operationId": "updatePassword", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PasswordUpdateDTO" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successfully updated password", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/UserDTO" - } - } - } - } - } - } - }, "/bank/v1/account/accounts/ssn/{ssn}": { "get": { "tags": [ @@ -711,8 +690,8 @@ } ], "responses": { - "404": { - "description": "Bank profile id does not exist", + "200": { + "description": "No accounts associated with a bank user", "content": { "*/*": { "schema": { @@ -724,8 +703,8 @@ } } }, - "200": { - "description": "No accounts associated with a bank user", + "404": { + "description": "Bank profile id does not exist", "content": { "*/*": { "schema": { @@ -891,7 +870,7 @@ } } }, - "/api/images/{id}": { + "/api/image/{id}": { "get": { "tags": [ "Image" @@ -916,24 +895,16 @@ "content": { "*/*": { "schema": { - "type": "string", - "format": "binary" - } - } - } - }, - "404": { - "description": "Image not found", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/ExceptionResponse" + "type": "array", + "items": { + "type": "string", + "format": "byte" + } } } } } - }, - "security": [] + } } }, "/api/goal/getGoals": { @@ -1024,8 +995,8 @@ } ], "responses": { - "500": { - "description": "Budget is not found", + "200": { + "description": "Successfully got budget", "content": { "application/json": { "schema": { @@ -1034,8 +1005,8 @@ } } }, - "200": { - "description": "Successfully got budget", + "500": { + "description": "Budget is not found", "content": { "application/json": { "schema": { @@ -1146,8 +1117,8 @@ } ], "responses": { - "500": { - "description": "Budget is not found", + "200": { + "description": "Successfully deleted budget", "content": { "application/json": { "schema": { @@ -1156,8 +1127,8 @@ } } }, - "200": { - "description": "Successfully deleted budget", + "500": { + "description": "Budget is not found", "content": { "application/json": { "schema": { @@ -1681,10 +1652,6 @@ "email": { "type": "string" }, - "profileImage": { - "type": "integer", - "format": "int64" - }, "checkingAccount": { "$ref": "#/components/schemas/Account" }, @@ -1835,9 +1802,8 @@ "email": { "type": "string" }, - "profileImage": { - "type": "integer", - "format": "int64" + "password": { + "type": "string" }, "configuration": { "$ref": "#/components/schemas/ConfigurationDTO" @@ -1857,10 +1823,6 @@ "lastName": { "type": "string" }, - "profileImage": { - "type": "integer", - "format": "int64" - }, "email": { "type": "string" }, @@ -1885,17 +1847,6 @@ } } }, - "PasswordUpdateDTO": { - "type": "object", - "properties": { - "oldPassword": { - "type": "string" - }, - "newPassword": { - "type": "string" - } - } - }, "ProfileDTO": { "type": "object", "properties": { @@ -1909,10 +1860,6 @@ "lastName": { "type": "string" }, - "profileImage": { - "type": "integer", - "format": "int64" - }, "createdAt": { "type": "string", "format": "date-time" diff --git a/src/components/Budget/BudgetBox.vue b/src/components/Budget/BudgetBox.vue index 88de9bbde4b0b5b952f71f29478de3395931f573..41a1c56296cadc428314bae52730e13dff3ec6c2 100644 --- a/src/components/Budget/BudgetBox.vue +++ b/src/components/Budget/BudgetBox.vue @@ -1,13 +1,21 @@ <script setup lang="ts"> import { onMounted, ref } from 'vue' -import { useRouter } from 'vue-router' +import ConfirmDeleteModal from '@/components/Budget/Modal/ConfirmDeleteModal.vue' -const router = useRouter(); +const emit = defineEmits(['deletedBudgetEvent', 'budgetPressedEvent']) const props = defineProps({ + id: { + type: Number, + required: true + }, title: { type: String, default: '' }, + createdAt: { + type: String, + default: '' + }, budget: { type: Number, default: 0 @@ -35,22 +43,36 @@ onMounted(() => { } }) -// TODO consider store chosen budget in a pinia store + /** - * Navigates to the pressed budget + * Navigates to the pressed budget with its id. */ const onBudgetContainerPressed = () => { - router.push('/budget') + emit('budgetPressedEvent', props.id) +} + +/** + * Emits an event to tell parent component to delete budget with its id. + */ +const onBudgetDeleted = () => { + emit('deletedBudgetEvent'); } </script> <template> + <confirm-delete-modal :budget-id="id" + :modal-id="String(id)" + :budgetTitle="title" + @deletedEvent="onBudgetDeleted"/> + <div class="container-fluid row" @click="onBudgetContainerPressed"> <div class="col-12"> <div class="title-container"> <h2>{{title}}</h2> + <p>Created {{createdAt.substring(0, 10).replace(/-/g, "/")}}</p> </div> + <button id="deleteButton" class="btn btn-danger" data-bs-toggle="modal" :data-bs-target="'#' + id" @click.stop=";"><img src="../../assets/icons/trash-can.svg" height="20" width="20" alt="picture">Delete</button> </div> <div class="col-4 budget"> @@ -135,8 +157,22 @@ div.col-4 { display: grid; grid-template-columns: 1fr 1fr; border-radius: 10px; - margin: 10px 0; + margin: 5px 0; } +div.col-12 { + display: grid; + grid-template-columns: 1fr 1fr; +} +div.col-12 p { + margin: 0; + padding: 0; +} + +#deleteButton { + z-index: 999; + align-self: center; + justify-self: right; +} </style> \ No newline at end of file diff --git a/src/components/Budget/ExpenseBox.vue b/src/components/Budget/ExpenseBox.vue index 082172a504595be0fb6f9ebd0553e9e94206b375..1100cd486f9900e90b196e457a96a37d36eb8fcb 100644 --- a/src/components/Budget/ExpenseBox.vue +++ b/src/components/Budget/ExpenseBox.vue @@ -1,9 +1,12 @@ <script setup lang="ts"> -import Button1 from '@/components/Buttons/Button1.vue' -import { type CreateAppFunction, ref } from 'vue' +import { ref } from 'vue' const emit = defineEmits(['deleteEvent', 'editEvent']); const props = defineProps({ + id: { + type: Number, + required: true + }, index: { type: Number, default: 0 @@ -19,24 +22,24 @@ const props = defineProps({ }) // Reactive variables for expense description and amount -let editDescription = ref('') -let editAmount = ref('') +let editDescription = ref(props.description) +let editAmount = ref(props.amount) /** * Emits an event to parent component with the type 'deleteEvent' to signalize - * that an expense with index 'index' must be removed. + * that an expense with given id must be removed. */ const emitDeleteEvent = () => { - emit('deleteEvent', props.index) + emit('deleteEvent', props.id) } /** * Emits an event to parent component with the type 'editEvent' to signalize - * that an expense with index 'index' is to be edited with the values 'editDescription' + * that an expense with given id is to be edited with the values 'editDescription' * and 'editAmount' */ const emitEditEvent = () => { - emit('editEvent', props.index, editDescription.value, editAmount.value) + emit('editEvent', props.id, editDescription.value, editAmount.value) } </script> @@ -59,7 +62,7 @@ const emitEditEvent = () => { <div class="container collapse-container"> <form @submit.prevent="emitEditEvent"> <div class="input-group"> - <span class="input-group-text">Edit expense #{{ index+1 }}: </span> + <span class="input-group-text">Edit expense {{ index+1 }} </span> <input type="text" class="form-control" placeholder="Expense description" required v-model="editDescription"> <input type="number" min="0" class="form-control" placeholder="Amount (kr)" required v-model="editAmount"> <button type="submit" class="btn btn-primary" data-bs-toggle="collapse" :data-bs-target="'#' + index">Confirm</button> @@ -70,6 +73,10 @@ const emitEditEvent = () => { </template> <style scoped> +div.collapse { + margin-bottom: 10px; +} + .expense-container { padding: 0 10px; display: grid; diff --git a/src/components/Budget/Modal/ConfirmDeleteModal.vue b/src/components/Budget/Modal/ConfirmDeleteModal.vue new file mode 100644 index 0000000000000000000000000000000000000000..a0966b9aa6ac4edb4e67aafa078d942897519d64 --- /dev/null +++ b/src/components/Budget/Modal/ConfirmDeleteModal.vue @@ -0,0 +1,62 @@ +<script setup lang="ts"> +import { UserService } from '@/api' + +const emit = defineEmits(['errorEvent', 'deletedEvent']) +const props = defineProps({ + modalId: { + type: String, + required: true + }, + budgetId: { + type: Number, + required: true + }, + budgetTitle: { + type: String, + default: '' + } +}) + +/** + * Deletes a budget with the specified ID and emits a delete event. + */ +const deleteBudget = async () => { + try { + await UserService.deleteBudget({budgetId: props.budgetId}) + emit('deletedEvent') + } catch (error) { + emit('errorEvent', error) + } +} + +</script> + +<template> + <div class="modal fade" :id="modalId"> + <div class="modal-dialog modal-dialog-centered"> + <div class="modal-content"> + <div class="modal-header"> + <h3>Are you sure you want to delete this budget <i>{{ budgetTitle }}?</i></h3> + <button class="btn btn-close" data-bs-dismiss="modal"></button> + </div> + <div class="modal-body"> + <button class="btn btn-primary" data-bs-dismiss="modal" @click="deleteBudget">Yes</button> + <button class="btn btn-primary" data-bs-dismiss="modal">No</button> + </div> + </div> + </div> + </div> +</template> + +<style scoped> + +.modal-header { + display: flex; +} + +.modal-body { + display: grid; + gap: 10px +} + +</style> \ No newline at end of file diff --git a/src/components/Budget/Modal/ImportBudgetModal.vue b/src/components/Budget/Modal/ImportBudgetModal.vue new file mode 100644 index 0000000000000000000000000000000000000000..47ab56b45c0d83de337a09af0cbd880bbd656cc2 --- /dev/null +++ b/src/components/Budget/Modal/ImportBudgetModal.vue @@ -0,0 +1,58 @@ +<script setup lang="ts"> +import type { BudgetResponseDTO } from '@/api' +import MiniBudgetBox from '@/components/Budget/Modal/MiniBudgetBox.vue' + +const emit = defineEmits(['importBudgetEvent']) +const props = defineProps({ + modalId: { + type: String, + required: true + }, + listOfBudgetResponseDTO: { + type: Array as () => BudgetResponseDTO[], + default: () => [] + } +}) + +/** + * Emits an importBudgetEvent to the parent in order to signalize that + * a budget with id has been imported. + * + * @param budgetId The id of the imported budget. + */ +const emitImportBudgetEvent = (budgetId: number) => { + emit('importBudgetEvent', budgetId) +} +</script> + +<template> + <div class="modal fade" :id="modalId"> + <div class="modal-dialog modal-dialog-centered modal-lg"> + <div class="modal-content"> + <div class="modal-header"> + <h3>Choose a budget you would like to import</h3> + <button class="btn btn-close" data-bs-dismiss="modal"></button> + </div> + <div class="modal-body"> + <MiniBudgetBox v-for="(item, index) in listOfBudgetResponseDTO" + :key="index" + :budget-id="Number(item.id) || 0" + :budget-title="item.budgetName" + :budget-amount="Number(item.budgetAmount)" + :expense-amount="Number(item.expenseAmount)" + @importBudgetEvent="emitImportBudgetEvent" + > + </MiniBudgetBox> + </div> + </div> + </div> + </div> +</template> + +<style scoped> + +div.modal-body { + padding-left: 0; +} + +</style> \ No newline at end of file diff --git a/src/components/Budget/Modal/MiniBudgetBox.vue b/src/components/Budget/Modal/MiniBudgetBox.vue new file mode 100644 index 0000000000000000000000000000000000000000..ef36d7c092890128e1bf041dd88a554fc6556b67 --- /dev/null +++ b/src/components/Budget/Modal/MiniBudgetBox.vue @@ -0,0 +1,121 @@ +<script setup lang="ts"> +import { ref } from 'vue' + +const emit = defineEmits(['importBudgetEvent']) +const props = defineProps({ + budgetId: { + type: Number, + required: true + }, + budgetTitle: { + type: String, + default: '' + }, + budgetAmount: { + type: Number, + default: 0 + }, + expenseAmount: { + type: Number, + default: 0 + } +}) +const balance = ref<number>(props.budgetAmount - props.expenseAmount); + +/** + * Emits an importBudgetEvent to the parent in order to signalize that + * a budget with id has been chosen to be imported. + */ +const emitImportBudgetEvent = () => { + emit('importBudgetEvent', props.budgetId) +} + +</script> + +<template> + <div class="container-fluid" @click="emitImportBudgetEvent"> + <h3>{{budgetTitle}}</h3> + + <div class="info budget"> + <i> + <img src="../../../assets/icons/money2.svg" width="30px" height="30px"> + </i> + <div class="amount budget-container"> + <h5>{{budgetAmount}} kr</h5> + </div> + </div> + + <div class="info expenses"> + <i> + <img src="../../../assets/icons/credit-card.svg" width="30px" height="30px"> + </i> + <div class="amount expenses-container"> + <h5>{{expenseAmount}} kr</h5> + </div> + </div> + + <div class="info balance"> + <i ref="iRef"> + <img src="../../../assets/icons/scale.svg" width="30px" height="30px"> + </i> + <div class="amount balance-container"> + <h5>{{balance}} kr</h5> + </div> + </div> + + </div> +</template> + +<style scoped> +div.container-fluid { + display: grid; + grid-template-columns: 0.4fr 0.3fr 0.3fr 0.3fr; + margin: 4px 9px; + border-radius: 10px; + background-color: #2a2a34; + cursor: pointer; + transition: transform 150ms ease-in-out; +} + +div.container-fluid:hover { + transform: scale(1.02); +} + +div.info { + display: grid; + grid-template-columns: 1fr 2fr; +} + +div.amount { + align-content: center; +} + +h3, h5 { + color: white; + margin-bottom: 0; + align-self: center; +} + +i { + display: grid; + justify-content: center; + align-content: center; + width: 40px; + height: 40px; + margin: 5px; + padding: 5px; + border-radius: 7px; +} + +.budget i { + background-color: rgba(78, 107, 239, 0.43); +} + +.expenses i { + background-color: rgba(238, 191, 43, 0.43); +} + +.balance i { + background-color: rgba(232, 14, 14, 0.43); +} +</style> \ No newline at end of file diff --git a/src/router/index.ts b/src/router/index.ts index 7620b659e79744d9cd791b358ca5fd219e6cc08c..2ccfc4126193fc6e595bd88598894ca01f696319 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -94,12 +94,12 @@ const routes = [ { path: '/budget-overview', name: 'budget overview', - component: () => import('@/views/BudgetOverview.vue'), + component: () => import('@/views/Budget/BudgetOverview.vue'), }, { path: '/budget', name: 'budget', - component: () => import('@/views/BudgetView.vue'), + component: () => import('@/views/Budget/BudgetView.vue'), }, { path: '/profile/:id', diff --git a/src/stores/BudgetStore.ts b/src/stores/BudgetStore.ts new file mode 100644 index 0000000000000000000000000000000000000000..0e6fce496f67816f6697d5cac49cd1b7750ec0a3 --- /dev/null +++ b/src/stores/BudgetStore.ts @@ -0,0 +1,20 @@ +import { defineStore } from 'pinia' + +export const useBudgetStore = defineStore('BudgetStore', { + state: () => ({ + activeBudgetId: 0, + }), + actions: { + setActiveBudgetId(id: number) { + this.activeBudgetId = id + } + }, + getters: { + getActiveBudgetId(): number { + return this.activeBudgetId + } + }, + persist: { + storage: sessionStorage + } +}); diff --git a/src/views/Budget/BudgetOverview.vue b/src/views/Budget/BudgetOverview.vue new file mode 100644 index 0000000000000000000000000000000000000000..3219b788013ccb0b54318572cab0401f8a259a84 --- /dev/null +++ b/src/views/Budget/BudgetOverview.vue @@ -0,0 +1,144 @@ +<script setup lang="ts"> +import Button1 from '@/components/Buttons/Button1.vue' +import BudgetBox from '@/components/Budget/BudgetBox.vue' +import { onMounted, ref } from 'vue' +import handleUnknownError from '@/components/Exceptions/unkownErrorHandler' +import { useBudgetStore } from '@/stores/BudgetStore' +import { type BudgetRequestDTO, type BudgetResponseDTO, UserService } from '@/api' +import { useRouter } from 'vue-router' + +const router = useRouter(); + +// Reactive list of budget responses +const budgetList = ref<BudgetResponseDTO[]>([]); +// Reactive variables for input value, error message, modal, and budgetList key +let budgetNameInput = ref('') +let errorMsg = ref(''); +let budgetListKey = ref(0); + +/** + * Attempts to retrieve budgets for the user asynchronously and updates + * the budget list state after rendering. + * Handles any errors by updating the error message state. + */ +onMounted(async () => { + try { + budgetList.value = await UserService.getBudgetsByUser() + console.log(budgetList.value) + } catch (error) { + errorMsg.value = handleUnknownError(error); + } +}) + +/** + * Creates a new budget to the database and updates the displayed budget list. + */ +const createNewBudget = async() => { + try { + // Prepare request body for creating budget + const request: BudgetRequestDTO = { + budgetName: budgetNameInput.value, + budgetAmount: 0, + expenseAmount: 0 + } + // Creates new budget with the budget request body + await UserService.createBudget({requestBody: request}) + // Updates displayed budget list after creation + await updateBudgetList() + } catch (error) { + errorMsg.value = handleUnknownError(error); + } +} + +/** + * Updates the displayed budget list. + */ +const updateBudgetList = async () => { + budgetList.value = await UserService.getBudgetsByUser() + budgetListKey.value++ +} + +/** + * Navigates to the budget page with the specified ID. + * + * @param {number} id The ID of the budget to navigate to. + */ +const goToBudget = (id: number) => { + useBudgetStore().setActiveBudgetId(id); + router.push("/budget") +} +</script> + +<template> + <div class="container"> + <h1 class="text-center">Your Budgets</h1> + <button1 id="createBudgetButton" button-text="Create new budget" class="btn btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#collapseExample" aria-expanded="false" aria-controls="collapseExample"/> + + <div class="collapse" id="collapseExample"> + <div class="container collapse-container"> + <div class="input-group"> + <input id="collapseInput" class="form-control" type="text" placeholder="Enter name of budget" v-model="budgetNameInput"> + <button1 id="collapseButton" button-text="Create" data-bs-dismiss="modal" @click="createNewBudget"/> + </div> + </div> + </div> + + <p class="text-danger">{{ errorMsg }}</p> + + <ul class="budgetContainer" :key="budgetListKey"> + <li v-for="(item, index) in budgetList"> + <budget-box + :key="index" + :id="Number(item.id) || 0" + :title="item.budgetName" + :created-at="item.createdAt" + :budget="item.budgetAmount" + :expenses="item.expenseAmount" + @deletedBudgetEvent="updateBudgetList" + @budgetPressedEvent="goToBudget" + ></budget-box> + </li> + </ul> + + <nav id="navbar" aria-label="Page navigation example"> + <ul class="pagination"> + <li class="page-item"> + <a class="page-link" href="#" aria-label="Previous"> + <span aria-hidden="true">«</span> + </a> + </li> + <li class="page-item"><a class="page-link" href="#">1</a></li> + <li class="page-item"><a class="page-link" href="#">2</a></li> + <li class="page-item"><a class="page-link" href="#">3</a></li> + <li class="page-item"> + <a class="page-link" href="#" aria-label="Next"> + <span aria-hidden="true">»</span> + </a> + </li> + </ul> + </nav> + + </div> + +</template> + +<style scoped> +.collapse-container { + align-content: center; + justify-content: center; + justify-items: center; +} + +.container { + padding: 10px; +} + +.budgetContainer { + list-style: none; + padding-left: 10px; +} + +ul > li { + margin: 10px 0; +} +</style> \ No newline at end of file diff --git a/src/views/Budget/BudgetView.vue b/src/views/Budget/BudgetView.vue new file mode 100644 index 0000000000000000000000000000000000000000..b4da6a4564de0e1bd850bf8e78b81af63a4e08de --- /dev/null +++ b/src/views/Budget/BudgetView.vue @@ -0,0 +1,425 @@ +<script setup lang="ts"> +import { onMounted, ref } from 'vue' +import Button1 from '@/components/Buttons/Button1.vue' +import ExpenseBox from '@/components/Budget/ExpenseBox.vue' +import { useRouter } from 'vue-router' +import { useBudgetStore } from '@/stores/BudgetStore' +import type { BudgetResponseDTO, ExpenseRequestDTO, ExpenseResponseDTO } from '@/api' +import { UserService } from '@/api' +import handleUnknownError from '@/components/Exceptions/unkownErrorHandler' +import ConfirmDeleteModal from '@/components/Budget/Modal/ConfirmDeleteModal.vue' +import ImportBudgetModal from '@/components/Budget/Modal/ImportBudgetModal.vue' + +const router = useRouter(); + +// Reactive header values +let title = ref(''); +let budget = ref(0); +let expenses = ref(0); +let balance = ref(0); +// Reactive error message and form value +let errorMsg = ref(''); +let renameFormRef = ref(null) +// Reactive expense list +let expenseDTOList = ref<ExpenseResponseDTO[]>([]) +// Reactive import budget list +let budgetDTOList = ref<BudgetResponseDTO[]>([]) +// Reactive background variable +const iRef = ref<any>() +// Reactive input values +let budgetTitle = ref('') +let budgetValue = ref<any>() +let expenseDescription = ref('') +let expenseAmount = ref<any>() + +/** + * Executes necessary updates on component mount. + * Updates the header, expenses, balance asynchronously, + * and gets budgets that are available to import. + */ +onMounted(async () => { + try { + await updateHeader(); + await updateExpenses(); + await updateBalance(); + // Gets budgets which can be imported + budgetDTOList.value = await UserService.getBudgetsByUser(); + budgetDTOList.value = budgetDTOList.value.filter(item => item.id !== useBudgetStore().getActiveBudgetId); + } catch (error) { + errorMsg.value = handleUnknownError(error); + } +}) + +/** + * Updates the header information asynchronously based on the active budget. + * Fetches the budget details using the UserService and updates the title, + * budget amount, and expense amount accordingly. + */ +const updateHeader = async () => { + const budgetResponse: BudgetResponseDTO = await UserService.getBudget({budgetId: useBudgetStore().getActiveBudgetId}); + if (budgetResponse.budgetName != null) { + title.value = budgetResponse.budgetName; + } + if (budgetResponse.budgetAmount != null) { + budget.value = budgetResponse.budgetAmount; + } + if (budgetResponse.expenseAmount != null) { + expenses.value = budgetResponse.expenseAmount; + } +} + +/** + * Updates the list of expenses asynchronously based on the active budget. + * Fetches the expenses associated with the active budget using the UserService. + */ +const updateExpenses = async () => { + expenseDTOList.value = await UserService.getExpenses({budgetId: useBudgetStore().getActiveBudgetId}); + // Resets expenses and then re-calculates it + expenses.value = 0; + for (let expenseDTO of expenseDTOList.value) { + expenses.value += Number(expenseDTO.amount); + } +} + +/** + * Updates the balance and the belonging background color based on the budget and expenses. + */ +const updateBalance = async () => { + // Updates balance value and background + balance.value = budget.value - expenses.value + if (balance.value >= 0) { + iRef.value.style.backgroundColor = 'rgba(34, 231, 50, 0.43)'; + } else { + iRef.value.style.backgroundColor= 'rgba(232, 14, 14, 0.43)'; + } +} + +/** + * Updates the budget information asynchronously with the provided new budget amount and name. + * Updates the local budget and title values, then sends a request to update the budget information + * using the UserService. + * + * @param {number} newBudget - The new budget amount to set. + * @param {string} newBudgetName - The new budget name to set. + */ +const updateBudget = async (newBudget: number, newBudgetName: string) => { + try { + budget.value = newBudget; + title.value = newBudgetName; + // Prepare request body for updating budget + const request: BudgetResponseDTO = { + budgetName: title.value, + budgetAmount: budget.value, + expenseAmount: expenses.value + } + // Send request to update budget information + await UserService.updateBudget({budgetId: useBudgetStore().getActiveBudgetId, requestBody: request}) + } catch (error) { + errorMsg.value = handleUnknownError(error) + } +} + +/** + * Adds a new expense with the provided description and value to the active budget. + * Sends a request to update the expense information using the UserService. + * Subsequently, triggers updates of the expenses, budget, and the balance. + * + * @param {string} expenseDescription - The description of the new expense. + * @param {number} expenseValue - The value of the new expense. + */ +const addNewExpense = async (expenseDescription: string, expenseValue: number) => { + try { + // Prepare request body for adding new expense + const request: ExpenseRequestDTO = { + description: expenseDescription, + amount: expenseValue + } + // Send request to update expense information + await UserService.updateExpense({budgetId: useBudgetStore().getActiveBudgetId, requestBody: request}); + // Trigger updates of expenses and balance and budget + await updateExpenses(); + await updateBudget(budget.value, title.value) + await updateBalance(); + } catch (error) { + errorMsg.value = handleUnknownError(error); + } +} + +/** + * Deletes an expense from the list of expenses. + * Sends a request to the UserService to delete the expense. + * Subsequently, triggers updates of the expenses, budget, and the balance. + * + * @param {number} id - The ID of the expense to delete. + */ +const deleteExpense = async (id: number) => { + try { + await UserService.deleteExpense({expenseId: id}); + await updateExpenses(); + await updateBudget(budget.value, title.value) + await updateBalance(); + } catch (error) { + errorMsg.value = handleUnknownError(error); + } +} + +/** + * Edits the details of an expense with the specified ID. + * Sends a request to the UserService to update the expense with new description and amount. + * Subsequently, triggers updates of the expenses and the balance. + * + * @param {number} id - The ID of the expense to edit. + * @param {string} newDescription - The new description for the expense. + * @param {number} newAmount - The new amount for the expense. + */ +const editExpense = async (id: number, newDescription: string, newAmount: number) => { + try { + // Prepare request body with updated details + const request: ExpenseRequestDTO = { + expenseId: id, + description: newDescription, + amount: newAmount + } + // Send request to update the expense using the UserService + await UserService.updateExpense({budgetId: useBudgetStore().getActiveBudgetId, requestBody: request}); + await updateExpenses(); + await updateBudget(budget.value, title.value) + await updateBalance(); + } catch (error) { + errorMsg.value = handleUnknownError(error); + } +} + +/** + * Imports a budget by updating the current budget with the data from the specified budget ID. + * + * @param {number} budgetId - The ID of the budget to import. + */ +const importBudget = async (budgetId: number) => { + try { + // Update current budget value from the imported budget + const budgetResponse: BudgetResponseDTO = await UserService.getBudget({budgetId: budgetId}); + if (budgetResponse.budgetAmount != null) { + budget.value += budgetResponse.budgetAmount; + } + // Get all the expenses from imported budget, and copy them to current budget + const expenses: ExpenseResponseDTO[] = await UserService.getExpenses({budgetId: budgetId}) + for (let expense of expenses) { + const expenseRequest: ExpenseRequestDTO = { + description: expense.description, + amount: expense.amount + } + await UserService.updateExpense({budgetId: useBudgetStore().getActiveBudgetId, requestBody: expenseRequest}); + } + // Update display and budget + await updateExpenses(); + await updateBudget(budget.value, title.value) + await updateBalance(); + } catch (error) { + errorMsg.value = handleUnknownError(error) + } +} +</script> + +<template> + <div class="container"> + <h1 class="text-center">{{ title }}</h1> + + <div class="button-container"> + <button1 id="goBack" @click="router.push('/budget-overview')" button-text="Go back"/> + <button1 id="optionButton" button-text="Options" data-bs-toggle="modal" data-bs-target="#modal"/> + </div> + + <p class="text-danger">{{ errorMsg }}</p> + + <div class="modal fade" id="modal"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h3>Options</h3> + <button class="btn btn-close" data-bs-dismiss="modal"></button> + </div> + <div class="modal-body"> + <button id="importButton" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#import-modal"><img src="../../assets/icons/import.svg" height="20" width="20" alt="picture">Import budget</button> + <button id="editBudget" class="btn btn-primary" data-bs-toggle="collapse" data-bs-target="#editBudgetCollapse" aria-expanded="false" aria-controls="editBudgetCollapse"><img src="../../assets/icons/edit-button.svg" alt="editButton">Rename budget</button> + <div class="collapse" id="editBudgetCollapse"> + <div class="container collapse-container"> + <form ref="renameFormRef" @submit.prevent="updateBudget(budget, budgetTitle)"> + <div class="input-group"> + <input id="collapseInput" class="col-5 form-control" type="text" required minlength="1" placeholder="Enter new name of budget" v-model="budgetTitle"> + <button1 id="collapseButton" type="submit" button-text="Confirm" data-bs-dismiss="modal"/> + </div> + </form> + </div> + </div> + <button id="deleteButton" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#confirm-modal"><img src="../../assets/icons/trash-can.svg" height="20" width="20" alt="picture">Delete budget</button> + </div> + </div> + </div> + </div> + + <confirm-delete-modal :budget-id="useBudgetStore().getActiveBudgetId" + modal-id="confirm-modal" + :budgetTitle="title" + @deletedEvent="router.push('/budget-overview')"/> + + <import-budget-modal modal-id="import-modal" + :listOfBudgetResponseDTO="budgetDTOList" + @importBudgetEvent="importBudget"/> + + <div class="budget-info-container"> + <div class="info budget-container"> + <i><img src="../../assets/icons/money2.svg" width="48px" height="48px" alt="picture"></i> + <div class="budget-text-container"> + <h5>{{budget}} kr</h5> + <p>Budget</p> + </div> + </div> + + <div class="info expenses-container"> + <i><img src="../../assets/icons/credit-card.svg" width="48px" height="48px" alt="picture"></i> + <div class="expenses-text-container"> + <h5>{{expenses}} kr</h5> + <p>Expenses</p> + </div> + </div> + + <div class="info balance-container"> + <i ref="iRef"><img src="../../assets/icons/scale.svg" width="48px" height="48px" alt="picture"></i> + <div class="balance-text-container"> + <h5>{{balance}} kr</h5> + <p>Balance</p> + </div> + </div> + </div> + + + <div class="budget-content-container"> + <form class="budget-from" @submit.prevent="updateBudget(budgetValue, title)"> + <div class="input-group"> + <span class="input-group-text">Your budget </span> + <input type="text" class="form-control" placeholder="Enter your budget" required v-model="budgetValue"> + <button type="submit" class="btn btn-primary">Calculate</button> + </div> + </form> + + <form class="expenses-form" @submit.prevent="addNewExpense(expenseDescription, expenseAmount)"> + <div class="input-group"> + <span class="input-group-text">Add new expense </span> + <input type="text" class="form-control" placeholder="Name of expense" required v-model="expenseDescription"> + <input type="number" min="0" class="form-control" placeholder="Amount (kr)" required v-model="expenseAmount"> + <button type="submit" class="btn btn-primary">Calculate</button> + </div> + </form> + </div> + + <div v-if="expenseDTOList.length != 0" class="expenses-details-container"> + <h3>Expenses details</h3> + <div class="expense-box-container"> + <expense-box v-for="(expenseDTO, index) in expenseDTOList" + :id="Number(expenseDTO.expenseId) || 0" + :key="index" + :index="index" + :description="expenseDTO.description" + :amount="Number(expenseDTO.amount) || 0" + @deleteEvent="deleteExpense" + @editEvent="editExpense"/> + </div> + </div> + + </div> +</template> + +<style scoped> + +.button-container { + display: flex; + gap: 10px; +} + +.container.collapse-container { + padding: 0; + margin: 0; +} + +.modal-header { + display: flex; +} + +.modal-body { + display: grid; + gap: 10px +} + +div.budget-info-container { + margin-top: 2rem; + display: grid; + grid-template-columns: 1fr 1fr 1fr; + justify-content: center; + gap: 1rem; +} + +div.info { + display: flex; + flex-direction: row; + background-color: rgba(221, 221, 224, 0.5); + border-radius: 10px; + padding: 10px; + transition: transform 150ms ease-in-out; +} + +div.info:hover { + transform: scale(1.03); +} + +.info i { + display: grid; + justify-content: center; + align-content: center; + margin: 5px; + border-radius: 7px; + min-width: 90px; +} + +.budget-container i { + background-color: rgba(78, 107, 239, 0.43); +} + +.expenses-container i { + background-color: rgba(238, 191, 43, 0.43); +} + +.balance-container i { + background-color: rgba(232, 14, 14, 0.43); +} + +.budget-content-container { + margin: 2rem 0; + display: grid; + gap: 5px; +} +.budget-content-container label { + display: flex; + align-items: center; +} + + +.expenses-details-container { + margin: 1rem 0; + min-height: 80px; + border-radius: 8px; + background-color: rgba(234, 234, 234, 0.8); +} + +.expenses-details-container h3 { + margin-top: 1rem; + padding: 10px; +} + +.expense-box-container { + overflow-y: auto; + overflow-x: hidden; + max-height: 100vh; +} + +</style> \ No newline at end of file diff --git a/src/views/BudgetOverview.vue b/src/views/BudgetOverview.vue deleted file mode 100644 index 617c79bb0bf6084454c6ca61e8e8238cd8deea70..0000000000000000000000000000000000000000 --- a/src/views/BudgetOverview.vue +++ /dev/null @@ -1,77 +0,0 @@ -<script setup lang="ts"> -import Button1 from '@/components/Buttons/Button1.vue' -import BudgetBox from '@/components/Budget/BudgetBox.vue' - -const budget = 1000; -const expenses = 95600; -</script> - -<template> - <div class="container"> - <h1 class="text-center">Your Budgets</h1> - <button1 id="createBudgetButton" button-text="Create new budget" class="btn btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#collapseExample" aria-expanded="false" aria-controls="collapseExample"/> - - <div class="collapse" id="collapseExample"> - <div class="container collapse-container"> - <div class="input-group row"> - <input id="collapseInput" class="col-5 form-control" type="text" placeholder="Enter name of budget"> - <button1 id="collapseButton" class="col-1" button-text="Create" data-bs-dismiss="modal"/> - </div> - </div> - </div> - - <!--TODO make this more generic--> - <ul class="budgetContainer"> - <li><budget-box title="April 2024" :budget=budget :expenses=expenses></budget-box></li> - <li><budget-box title="Mai 2024" :budget=budget :expenses=expenses></budget-box></li> - <li><budget-box title="Juni 2024" :budget=budget :expenses=expenses></budget-box></li> - <li><budget-box title="Juli 2024" :budget=budget :expenses=expenses></budget-box></li> - <li><budget-box title="August 2024" :budget=budget :expenses=expenses></budget-box></li> - <li><budget-box title="September 2024" :budget=budget :expenses=expenses></budget-box></li> - <li><budget-box title="Oktober 2024" :budget=budget :expenses=expenses></budget-box></li> - <li><budget-box title="November 2024" :budget=budget :expenses=expenses></budget-box></li> - <li><budget-box title="Desember 2024" :budget=budget :expenses=expenses></budget-box></li> - </ul> - - <nav id="navbar" aria-label="Page navigation example"> - <ul class="pagination"> - <li class="page-item"> - <a class="page-link" href="#" aria-label="Previous"> - <span aria-hidden="true">«</span> - </a> - </li> - <li class="page-item"><a class="page-link" href="#">1</a></li> - <li class="page-item"><a class="page-link" href="#">2</a></li> - <li class="page-item"><a class="page-link" href="#">3</a></li> - <li class="page-item"> - <a class="page-link" href="#" aria-label="Next"> - <span aria-hidden="true">»</span> - </a> - </li> - </ul> - </nav> - - </div> - -</template> - -<style scoped> -.collapse-container { - align-content: center; - justify-content: center; - justify-items: center; -} - -.container { - padding: 10px; -} - -.budgetContainer { - list-style: none; - padding-left: 10px; -} - -ul > li { - margin: 10px 0; -} -</style> \ No newline at end of file diff --git a/src/views/BudgetView.vue b/src/views/BudgetView.vue deleted file mode 100644 index 750d0e16a3140ba26aee608bf5c1d77c609d78a1..0000000000000000000000000000000000000000 --- a/src/views/BudgetView.vue +++ /dev/null @@ -1,328 +0,0 @@ -<script setup lang="ts"> -import { onMounted, ref } from 'vue' -import Button1 from '@/components/Buttons/Button1.vue' -import ExpenseBox from '@/components/Budget/ExpenseBox.vue' -import router from '@/router' - -// TODO Need endpoint in order to retrieve budget -// Mocked values -let title = ref('Mai 2024'); -let budget = ref(10000); -let expenses = ref(0); -let balance = ref(budget.value - expenses.value); -let expenseJSONObject = ref({ - "expenses": [ - { - "title": "Ost", - "value": 30 - }, - { - "title": "Skinke", - "value": 20 - }, - { - "title": "Bread", - "value": 15 - } - ] -}); - -// Initially updates the total expense display -for (let expense of expenseJSONObject.value.expenses) { - expenses.value += expense.value -} - -// Reactive input values -let budgetTitle = ref('') -let budgetValue = ref() -let expenseDescription = ref('') -let expenseAmount = ref() -// Reactive background variable -const iRef = ref() - -/** - * Checks the value of the balance and adjust background color depending - * on negative or positive value after rendering. - */ -onMounted(() => { - if (balance.value >= 0) { - iRef.value.style.backgroundColor = 'rgba(34, 231, 50, 0.43)'; - } - balance.value = budget.value - expenses.value -}) - -/** - * Updates the balance and background color based on the budget and expenses. - */ -const updateBalance = () => { - // Resets expenses and then re-calculates it - expenses.value = 0 - for (let expense of expenseJSONObject.value.expenses) { - expenses.value += expense.value - } - // Updates balance value and background - balance.value = budget.value - expenses.value - if (balance.value >= 0) { - iRef.value.style.backgroundColor = 'rgba(34, 231, 50, 0.43)'; - } else { - iRef.value.style.backgroundColor= 'rgba(232, 14, 14, 0.43)'; - } -} - -/** - * Calculates a new budget and updates the balance. - * - * @param newBudget The new budget value. - */ -const calculateNewBudget = (newBudget: number) => { - budget.value = newBudget - updateBalance() -} - -/** - * TODO update javadoc when backend integration is done - * Adds a new expense to the expense JSON object and updates the balance. - * - * @param expenseDescription The description of the expense. - * @param expenseValue The value of the expense. - */ -const addNewExpense = (expenseDescription: string, expenseValue: number) => { - expenseJSONObject.value.expenses.push({ - "title": expenseDescription, - "value": expenseValue - }); - updateBalance() -} - - -/** - * Updates the title of the budget. - * - * @param newTitle The new title for the budget. - */ -const editBudgetTitle = (newTitle: string) => { - title.value = newTitle -} - -/** - * Deletes an expense from the list of expenses. - * - * @param index The index of the expense to delete. - */ -const deleteExpense = (index: number) => { - expenseJSONObject.value.expenses.splice(index, 1); - updateBalance() -} - -/** - * Edits an existing expense in the list of expenses. - * - * @param index The index of the expense to edit. - * @param newDescription The new description for the expense. - * @param newAmount The new amount for the expense. - */ -const editExpense = (index: number, newDescription: string, newAmount: number) => { - console.log('Reached') - expenseJSONObject.value.expenses[index].title = newDescription - expenseJSONObject.value.expenses[index].value = newAmount - updateBalance() -} - -// TODO add delete functionality -const onDeleteBudgetPressed = () => { - router.push('/budget-overview') -} - -</script> - -<template> - <div class="container"> - <h1 class="text-center">{{ title }}</h1> - - <div class="button-container"> - <button1 id="optionButton" button-text="Options" data-bs-toggle="modal" - data-bs-target="#modal"/> - <button1 id="saveChanges" button-text="Save changes"/> - </div> - - <div class="modal fade" id="modal"> - <div class="modal-dialog"> - <div class="modal-content"> - <div class="modal-header"> - <h3>Options</h3> - <button class="btn btn-close" data-bs-dismiss="modal"></button> - </div> - <div class="modal-body"> - <button id="importButton" class="btn btn-primary"><img src="../assets/icons/import.svg" height="20" width="20" alt="picture">Import budget</button> - <button id="editBudget" class="btn btn-primary" data-bs-toggle="collapse" data-bs-target="#editBudgetCollapse" aria-expanded="false" aria-controls="editBudgetCollapse"><img src="../assets/icons/edit-button.svg" alt="editButton">Rename budget</button> - <div class="collapse" id="editBudgetCollapse"> - <div class="container collapse-container"> - <form @submit.prevent="editBudgetTitle(budgetTitle)"> - <div class="input-group"> - <input id="collapseInput" class="col-5 form-control" type="text" placeholder="Enter new name of budget" v-model="budgetTitle"> - <button1 id="collapseButton" type="submit" button-text="Edit" data-bs-dismiss="modal" /> - </div> - </form> - </div> - </div> - <button id="deleteButton" class="btn btn-primary" data-bs-toggle="modal" @click="onDeleteBudgetPressed"><img src="../assets/icons/trash-can.svg" height="20" width="20" alt="picture">Delete budget</button> - </div> - </div> - </div> - </div> - - <div class="budget-info-container"> - <div class="info budget-container"> - <i><img src="../assets/icons/money2.svg" width="48px" height="48px" alt="picture"></i> - <div class="budget-text-container"> - <h5>{{budget}} kr</h5> - <p>Budget</p> - </div> - </div> - - <div class="info expenses-container"> - <i><img src="../assets/icons/credit-card.svg" width="48px" height="48px" alt="picture"></i> - <div class="expenses-text-container"> - <h5>{{expenses}} kr</h5> - <p>Expenses</p> - </div> - </div> - - <div class="info balance-container"> - <i ref="iRef"><img src="../assets/icons/scale.svg" width="48px" height="48px" alt="picture"></i> - <div class="balance-text-container"> - <h5>{{balance}} kr</h5> - <p>Balance</p> - </div> - </div> - </div> - - - <div class="budget-content-container"> - <form class="budget-from" @submit.prevent="calculateNewBudget(budgetValue)"> - <div class="input-group"> - <span class="input-group-text">Your budget: </span> - <input type="text" class="form-control" placeholder="Enter your budget" required v-model="budgetValue"> - <button type="submit" class="btn btn-primary">Calculate</button> - </div> - </form> - - <form class="expenses-form" @submit.prevent="addNewExpense(expenseDescription, expenseAmount)"> - <div class="input-group"> - <span class="input-group-text">Add new expense: </span> - <input type="text" class="form-control" placeholder="Name of expense" required v-model="expenseDescription"> - <input type="number" min="0" class="form-control" placeholder="Amount (kr)" required v-model="expenseAmount"> - <button type="submit" class="btn btn-primary">Calculate</button> - </div> - </form> - </div> - - <div class="expenses-details-container"> - <h3>Expenses details</h3> - <div class="expense-box-container"> - <expense-box v-for="(expense, index) in expenseJSONObject.expenses" - :key="index" - :index="index" - :description="expense.title" - :amount="expense.value" - @deleteEvent="deleteExpense" - @editEvent="editExpense"/> - </div> - </div> - - </div> -</template> - -<style scoped> - -.button-container { - display: flex; - gap: 10px; -} - -.container.collapse-container { - padding: 0; - margin: 0; -} - -.modal-header { - display: flex; -} - -.modal-body { - display: grid; - gap: 10px -} - -div.budget-info-container { - margin-top: 2rem; - display: grid; - grid-template-columns: 1fr 1fr 1fr; - justify-content: center; - gap: 1rem; -} - -div.info { - display: flex; - flex-direction: row; - background-color: rgba(221, 221, 224, 0.5); - border-radius: 10px; - padding: 10px; - transition: transform 150ms ease-in-out; -} - -div.info:hover { - transform: scale(1.03); -} - -i { - display: grid; - justify-content: center; - align-content: center; - margin: 5px; - border-radius: 7px; - min-width: 90px; -} - -.budget-container i { - background-color: rgba(78, 107, 239, 0.43); -} - -.expenses-container i { - background-color: rgba(238, 191, 43, 0.43); -} - -.balance-container i { - background-color: rgba(232, 14, 14, 0.43); -} - -.budget-content-container { - margin: 2rem 0; - display: grid; - gap: 5px; -} -.budget-content-container label { - display: flex; - align-items: center; -} - - -.expenses-details-container { - margin: 1rem 0; - min-height: 80px; - border-radius: 8px; - background-color: rgba(234, 234, 234, 0.8); -} - -.expenses-details-container h3 { - margin-top: 1rem; - padding: 10px; -} - -.expense-box-container { - overflow-y: auto; - overflow-x: hidden; - max-height: 100vh; -} - -</style> \ No newline at end of file