Compare commits
No commits in common. "3c181c2e4595968e51622872376ecf55fdf59d75" and "0fc5b07f5edffef49426d3f8b7d1792a0e49cfbc" have entirely different histories.
3c181c2e45
...
0fc5b07f5e
62 changed files with 807 additions and 19278 deletions
50
.github/workflows/deploy.yml
vendored
50
.github/workflows/deploy.yml
vendored
|
@ -1,50 +0,0 @@
|
||||||
name: Deploy Vaadin App
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- prod
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up JDK 22
|
|
||||||
uses: actions/setup-java@v3
|
|
||||||
with:
|
|
||||||
java-version: '22'
|
|
||||||
distribution: 'temurin'
|
|
||||||
|
|
||||||
- name: Build JAR
|
|
||||||
run: ./mvnw clean package -Pproduction -DskipTests
|
|
||||||
|
|
||||||
- name: Set up SSH key
|
|
||||||
uses: webfactory/ssh-agent@v0.5.3
|
|
||||||
with:
|
|
||||||
ssh-private-key: ${{ secrets.SSH_KEY }}
|
|
||||||
|
|
||||||
#- name: Clean deployment directory on server
|
|
||||||
# run: ssh -o StrictHostKeyChecking=no ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} 'rm -rf /home/${{ secrets.SSH_USER }}/munera/*'
|
|
||||||
|
|
||||||
- name: Upload JAR to server
|
|
||||||
run: scp -o StrictHostKeyChecking=no target/munera-1.0-SNAPSHOT.jar ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/home/${{ secrets.SSH_USER }}/munera
|
|
||||||
|
|
||||||
- name: Upload Dockerfile to server
|
|
||||||
run: scp -o StrictHostKeyChecking=no Dockerfile ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/home/${{ secrets.SSH_USER }}/munera
|
|
||||||
|
|
||||||
- name: Upload docker-compose.yml to server
|
|
||||||
run: scp -o StrictHostKeyChecking=no docker-compose.yml ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/home/${{ secrets.SSH_USER }}/munera
|
|
||||||
|
|
||||||
- name: Stop Docker containers on server
|
|
||||||
run: ssh -o StrictHostKeyChecking=no ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} 'cd /home/${{ secrets.SSH_USER }}/munera && docker-compose down'
|
|
||||||
|
|
||||||
- name: Start Docker containers on server
|
|
||||||
run: |
|
|
||||||
ssh -o StrictHostKeyChecking=no ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} << 'ENDSSH'
|
|
||||||
cd /home/${{ secrets.SSH_USER }}/munera
|
|
||||||
docker-compose up --build -d
|
|
||||||
ENDSSH
|
|
41
.github/workflows/deploy.yml.bak
vendored
41
.github/workflows/deploy.yml.bak
vendored
|
@ -1,41 +0,0 @@
|
||||||
name: Deploy to Debian Server
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout the code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Set up SSH
|
|
||||||
uses: webfactory/ssh-agent@v0.5.3
|
|
||||||
with:
|
|
||||||
ssh-private-key: ${{ secrets.SSH_KEY }}
|
|
||||||
|
|
||||||
- name: Copy files to server
|
|
||||||
run: |
|
|
||||||
scp -o StrictHostKeyChecking=no docker-compose.yml Dockerfile ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/home/${{ secrets.SSH_USER }}/munera/
|
|
||||||
scp -o StrictHostKeyChecking=no target/munera-1.0-SNAPSHOT.jar ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/home/${{ secrets.SSH_USER }}/munera/
|
|
||||||
|
|
||||||
- name: Deploy on server
|
|
||||||
run: |
|
|
||||||
ssh -o StrictHostKeyChecking=no ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} << 'EOF'
|
|
||||||
cd /home/${{ secrets.SSH_USER }}/munera
|
|
||||||
echo "DB_ADDRESS=${DB_ADDRESS}" > .env
|
|
||||||
echo "DB_USER=${DB_USER}" >> .env
|
|
||||||
echo "DB_PASSWORD=${DB_PASSWORD}" >> .env
|
|
||||||
echo "DB_NAME=${DB_NAME}" >> .env
|
|
||||||
docker-compose down
|
|
||||||
docker-compose up -d --build
|
|
||||||
EOF
|
|
||||||
env:
|
|
||||||
DB_ADDRESS: ${{ secrets.DB_ADDRESS }}
|
|
||||||
DB_USER: ${{ secrets.DB_USER }}
|
|
||||||
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
|
|
||||||
DB_NAME: ${{ secrets.DB_NAME }}
|
|
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -4,12 +4,6 @@
|
||||||
.settings
|
.settings
|
||||||
.project
|
.project
|
||||||
.classpath
|
.classpath
|
||||||
.env
|
|
||||||
|
|
||||||
src/main/resources/*.sql
|
|
||||||
|
|
||||||
# Ignore profile-specific properties files
|
|
||||||
application-*.properties
|
|
||||||
|
|
||||||
*.iml
|
*.iml
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
13
Dockerfile
13
Dockerfile
|
@ -1,13 +0,0 @@
|
||||||
FROM openjdk:21-jdk-slim
|
|
||||||
|
|
||||||
# Set the working directory
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy the JAR file into the container
|
|
||||||
COPY munera-1.0-SNAPSHOT.jar /app/munera-1.0-SNAPSHOT.jar
|
|
||||||
|
|
||||||
# Expose the port the app runs on
|
|
||||||
EXPOSE 8080
|
|
||||||
|
|
||||||
# Run the JAR file
|
|
||||||
CMD ["java", "-jar", "/app/munera-1.0-SNAPSHOT.jar"]
|
|
661
LICENSE
661
LICENSE
|
@ -1,661 +0,0 @@
|
||||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
|
||||||
Version 3, 19 November 2007
|
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
|
||||||
of this license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
Preamble
|
|
||||||
|
|
||||||
The GNU Affero General Public License is a free, copyleft license for
|
|
||||||
software and other kinds of works, specifically designed to ensure
|
|
||||||
cooperation with the community in the case of network server software.
|
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
|
||||||
to take away your freedom to share and change the works. By contrast,
|
|
||||||
our General Public Licenses are intended to guarantee your freedom to
|
|
||||||
share and change all versions of a program--to make sure it remains free
|
|
||||||
software for all its users.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
|
||||||
have the freedom to distribute copies of free software (and charge for
|
|
||||||
them if you wish), that you receive source code or can get it if you
|
|
||||||
want it, that you can change the software or use pieces of it in new
|
|
||||||
free programs, and that you know you can do these things.
|
|
||||||
|
|
||||||
Developers that use our General Public Licenses protect your rights
|
|
||||||
with two steps: (1) assert copyright on the software, and (2) offer
|
|
||||||
you this License which gives you legal permission to copy, distribute
|
|
||||||
and/or modify the software.
|
|
||||||
|
|
||||||
A secondary benefit of defending all users' freedom is that
|
|
||||||
improvements made in alternate versions of the program, if they
|
|
||||||
receive widespread use, become available for other developers to
|
|
||||||
incorporate. Many developers of free software are heartened and
|
|
||||||
encouraged by the resulting cooperation. However, in the case of
|
|
||||||
software used on network servers, this result may fail to come about.
|
|
||||||
The GNU General Public License permits making a modified version and
|
|
||||||
letting the public access it on a server without ever releasing its
|
|
||||||
source code to the public.
|
|
||||||
|
|
||||||
The GNU Affero General Public License is designed specifically to
|
|
||||||
ensure that, in such cases, the modified source code becomes available
|
|
||||||
to the community. It requires the operator of a network server to
|
|
||||||
provide the source code of the modified version running there to the
|
|
||||||
users of that server. Therefore, public use of a modified version, on
|
|
||||||
a publicly accessible server, gives the public access to the source
|
|
||||||
code of the modified version.
|
|
||||||
|
|
||||||
An older license, called the Affero General Public License and
|
|
||||||
published by Affero, was designed to accomplish similar goals. This is
|
|
||||||
a different license, not a version of the Affero GPL, but Affero has
|
|
||||||
released a new version of the Affero GPL which permits relicensing under
|
|
||||||
this license.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
|
||||||
modification follow.
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
0. Definitions.
|
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
|
||||||
works, such as semiconductor masks.
|
|
||||||
|
|
||||||
"The Program" refers to any copyrightable work licensed under this
|
|
||||||
License. Each licensee is addressed as "you". "Licensees" and
|
|
||||||
"recipients" may be individuals or organizations.
|
|
||||||
|
|
||||||
To "modify" a work means to copy from or adapt all or part of the work
|
|
||||||
in a fashion requiring copyright permission, other than the making of an
|
|
||||||
exact copy. The resulting work is called a "modified version" of the
|
|
||||||
earlier work or a work "based on" the earlier work.
|
|
||||||
|
|
||||||
A "covered work" means either the unmodified Program or a work based
|
|
||||||
on the Program.
|
|
||||||
|
|
||||||
To "propagate" a work means to do anything with it that, without
|
|
||||||
permission, would make you directly or secondarily liable for
|
|
||||||
infringement under applicable copyright law, except executing it on a
|
|
||||||
computer or modifying a private copy. Propagation includes copying,
|
|
||||||
distribution (with or without modification), making available to the
|
|
||||||
public, and in some countries other activities as well.
|
|
||||||
|
|
||||||
To "convey" a work means any kind of propagation that enables other
|
|
||||||
parties to make or receive copies. Mere interaction with a user through
|
|
||||||
a computer network, with no transfer of a copy, is not conveying.
|
|
||||||
|
|
||||||
An interactive user interface displays "Appropriate Legal Notices"
|
|
||||||
to the extent that it includes a convenient and prominently visible
|
|
||||||
feature that (1) displays an appropriate copyright notice, and (2)
|
|
||||||
tells the user that there is no warranty for the work (except to the
|
|
||||||
extent that warranties are provided), that licensees may convey the
|
|
||||||
work under this License, and how to view a copy of this License. If
|
|
||||||
the interface presents a list of user commands or options, such as a
|
|
||||||
menu, a prominent item in the list meets this criterion.
|
|
||||||
|
|
||||||
1. Source Code.
|
|
||||||
|
|
||||||
The "source code" for a work means the preferred form of the work
|
|
||||||
for making modifications to it. "Object code" means any non-source
|
|
||||||
form of a work.
|
|
||||||
|
|
||||||
A "Standard Interface" means an interface that either is an official
|
|
||||||
standard defined by a recognized standards body, or, in the case of
|
|
||||||
interfaces specified for a particular programming language, one that
|
|
||||||
is widely used among developers working in that language.
|
|
||||||
|
|
||||||
The "System Libraries" of an executable work include anything, other
|
|
||||||
than the work as a whole, that (a) is included in the normal form of
|
|
||||||
packaging a Major Component, but which is not part of that Major
|
|
||||||
Component, and (b) serves only to enable use of the work with that
|
|
||||||
Major Component, or to implement a Standard Interface for which an
|
|
||||||
implementation is available to the public in source code form. A
|
|
||||||
"Major Component", in this context, means a major essential component
|
|
||||||
(kernel, window system, and so on) of the specific operating system
|
|
||||||
(if any) on which the executable work runs, or a compiler used to
|
|
||||||
produce the work, or an object code interpreter used to run it.
|
|
||||||
|
|
||||||
The "Corresponding Source" for a work in object code form means all
|
|
||||||
the source code needed to generate, install, and (for an executable
|
|
||||||
work) run the object code and to modify the work, including scripts to
|
|
||||||
control those activities. However, it does not include the work's
|
|
||||||
System Libraries, or general-purpose tools or generally available free
|
|
||||||
programs which are used unmodified in performing those activities but
|
|
||||||
which are not part of the work. For example, Corresponding Source
|
|
||||||
includes interface definition files associated with source files for
|
|
||||||
the work, and the source code for shared libraries and dynamically
|
|
||||||
linked subprograms that the work is specifically designed to require,
|
|
||||||
such as by intimate data communication or control flow between those
|
|
||||||
subprograms and other parts of the work.
|
|
||||||
|
|
||||||
The Corresponding Source need not include anything that users
|
|
||||||
can regenerate automatically from other parts of the Corresponding
|
|
||||||
Source.
|
|
||||||
|
|
||||||
The Corresponding Source for a work in source code form is that
|
|
||||||
same work.
|
|
||||||
|
|
||||||
2. Basic Permissions.
|
|
||||||
|
|
||||||
All rights granted under this License are granted for the term of
|
|
||||||
copyright on the Program, and are irrevocable provided the stated
|
|
||||||
conditions are met. This License explicitly affirms your unlimited
|
|
||||||
permission to run the unmodified Program. The output from running a
|
|
||||||
covered work is covered by this License only if the output, given its
|
|
||||||
content, constitutes a covered work. This License acknowledges your
|
|
||||||
rights of fair use or other equivalent, as provided by copyright law.
|
|
||||||
|
|
||||||
You may make, run and propagate covered works that you do not
|
|
||||||
convey, without conditions so long as your license otherwise remains
|
|
||||||
in force. You may convey covered works to others for the sole purpose
|
|
||||||
of having them make modifications exclusively for you, or provide you
|
|
||||||
with facilities for running those works, provided that you comply with
|
|
||||||
the terms of this License in conveying all material for which you do
|
|
||||||
not control copyright. Those thus making or running the covered works
|
|
||||||
for you must do so exclusively on your behalf, under your direction
|
|
||||||
and control, on terms that prohibit them from making any copies of
|
|
||||||
your copyrighted material outside their relationship with you.
|
|
||||||
|
|
||||||
Conveying under any other circumstances is permitted solely under
|
|
||||||
the conditions stated below. Sublicensing is not allowed; section 10
|
|
||||||
makes it unnecessary.
|
|
||||||
|
|
||||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
|
||||||
|
|
||||||
No covered work shall be deemed part of an effective technological
|
|
||||||
measure under any applicable law fulfilling obligations under article
|
|
||||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
|
||||||
similar laws prohibiting or restricting circumvention of such
|
|
||||||
measures.
|
|
||||||
|
|
||||||
When you convey a covered work, you waive any legal power to forbid
|
|
||||||
circumvention of technological measures to the extent such circumvention
|
|
||||||
is effected by exercising rights under this License with respect to
|
|
||||||
the covered work, and you disclaim any intention to limit operation or
|
|
||||||
modification of the work as a means of enforcing, against the work's
|
|
||||||
users, your or third parties' legal rights to forbid circumvention of
|
|
||||||
technological measures.
|
|
||||||
|
|
||||||
4. Conveying Verbatim Copies.
|
|
||||||
|
|
||||||
You may convey verbatim copies of the Program's source code as you
|
|
||||||
receive it, in any medium, provided that you conspicuously and
|
|
||||||
appropriately publish on each copy an appropriate copyright notice;
|
|
||||||
keep intact all notices stating that this License and any
|
|
||||||
non-permissive terms added in accord with section 7 apply to the code;
|
|
||||||
keep intact all notices of the absence of any warranty; and give all
|
|
||||||
recipients a copy of this License along with the Program.
|
|
||||||
|
|
||||||
You may charge any price or no price for each copy that you convey,
|
|
||||||
and you may offer support or warranty protection for a fee.
|
|
||||||
|
|
||||||
5. Conveying Modified Source Versions.
|
|
||||||
|
|
||||||
You may convey a work based on the Program, or the modifications to
|
|
||||||
produce it from the Program, in the form of source code under the
|
|
||||||
terms of section 4, provided that you also meet all of these conditions:
|
|
||||||
|
|
||||||
a) The work must carry prominent notices stating that you modified
|
|
||||||
it, and giving a relevant date.
|
|
||||||
|
|
||||||
b) The work must carry prominent notices stating that it is
|
|
||||||
released under this License and any conditions added under section
|
|
||||||
7. This requirement modifies the requirement in section 4 to
|
|
||||||
"keep intact all notices".
|
|
||||||
|
|
||||||
c) You must license the entire work, as a whole, under this
|
|
||||||
License to anyone who comes into possession of a copy. This
|
|
||||||
License will therefore apply, along with any applicable section 7
|
|
||||||
additional terms, to the whole of the work, and all its parts,
|
|
||||||
regardless of how they are packaged. This License gives no
|
|
||||||
permission to license the work in any other way, but it does not
|
|
||||||
invalidate such permission if you have separately received it.
|
|
||||||
|
|
||||||
d) If the work has interactive user interfaces, each must display
|
|
||||||
Appropriate Legal Notices; however, if the Program has interactive
|
|
||||||
interfaces that do not display Appropriate Legal Notices, your
|
|
||||||
work need not make them do so.
|
|
||||||
|
|
||||||
A compilation of a covered work with other separate and independent
|
|
||||||
works, which are not by their nature extensions of the covered work,
|
|
||||||
and which are not combined with it such as to form a larger program,
|
|
||||||
in or on a volume of a storage or distribution medium, is called an
|
|
||||||
"aggregate" if the compilation and its resulting copyright are not
|
|
||||||
used to limit the access or legal rights of the compilation's users
|
|
||||||
beyond what the individual works permit. Inclusion of a covered work
|
|
||||||
in an aggregate does not cause this License to apply to the other
|
|
||||||
parts of the aggregate.
|
|
||||||
|
|
||||||
6. Conveying Non-Source Forms.
|
|
||||||
|
|
||||||
You may convey a covered work in object code form under the terms
|
|
||||||
of sections 4 and 5, provided that you also convey the
|
|
||||||
machine-readable Corresponding Source under the terms of this License,
|
|
||||||
in one of these ways:
|
|
||||||
|
|
||||||
a) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by the
|
|
||||||
Corresponding Source fixed on a durable physical medium
|
|
||||||
customarily used for software interchange.
|
|
||||||
|
|
||||||
b) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by a
|
|
||||||
written offer, valid for at least three years and valid for as
|
|
||||||
long as you offer spare parts or customer support for that product
|
|
||||||
model, to give anyone who possesses the object code either (1) a
|
|
||||||
copy of the Corresponding Source for all the software in the
|
|
||||||
product that is covered by this License, on a durable physical
|
|
||||||
medium customarily used for software interchange, for a price no
|
|
||||||
more than your reasonable cost of physically performing this
|
|
||||||
conveying of source, or (2) access to copy the
|
|
||||||
Corresponding Source from a network server at no charge.
|
|
||||||
|
|
||||||
c) Convey individual copies of the object code with a copy of the
|
|
||||||
written offer to provide the Corresponding Source. This
|
|
||||||
alternative is allowed only occasionally and noncommercially, and
|
|
||||||
only if you received the object code with such an offer, in accord
|
|
||||||
with subsection 6b.
|
|
||||||
|
|
||||||
d) Convey the object code by offering access from a designated
|
|
||||||
place (gratis or for a charge), and offer equivalent access to the
|
|
||||||
Corresponding Source in the same way through the same place at no
|
|
||||||
further charge. You need not require recipients to copy the
|
|
||||||
Corresponding Source along with the object code. If the place to
|
|
||||||
copy the object code is a network server, the Corresponding Source
|
|
||||||
may be on a different server (operated by you or a third party)
|
|
||||||
that supports equivalent copying facilities, provided you maintain
|
|
||||||
clear directions next to the object code saying where to find the
|
|
||||||
Corresponding Source. Regardless of what server hosts the
|
|
||||||
Corresponding Source, you remain obligated to ensure that it is
|
|
||||||
available for as long as needed to satisfy these requirements.
|
|
||||||
|
|
||||||
e) Convey the object code using peer-to-peer transmission, provided
|
|
||||||
you inform other peers where the object code and Corresponding
|
|
||||||
Source of the work are being offered to the general public at no
|
|
||||||
charge under subsection 6d.
|
|
||||||
|
|
||||||
A separable portion of the object code, whose source code is excluded
|
|
||||||
from the Corresponding Source as a System Library, need not be
|
|
||||||
included in conveying the object code work.
|
|
||||||
|
|
||||||
A "User Product" is either (1) a "consumer product", which means any
|
|
||||||
tangible personal property which is normally used for personal, family,
|
|
||||||
or household purposes, or (2) anything designed or sold for incorporation
|
|
||||||
into a dwelling. In determining whether a product is a consumer product,
|
|
||||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
|
||||||
product received by a particular user, "normally used" refers to a
|
|
||||||
typical or common use of that class of product, regardless of the status
|
|
||||||
of the particular user or of the way in which the particular user
|
|
||||||
actually uses, or expects or is expected to use, the product. A product
|
|
||||||
is a consumer product regardless of whether the product has substantial
|
|
||||||
commercial, industrial or non-consumer uses, unless such uses represent
|
|
||||||
the only significant mode of use of the product.
|
|
||||||
|
|
||||||
"Installation Information" for a User Product means any methods,
|
|
||||||
procedures, authorization keys, or other information required to install
|
|
||||||
and execute modified versions of a covered work in that User Product from
|
|
||||||
a modified version of its Corresponding Source. The information must
|
|
||||||
suffice to ensure that the continued functioning of the modified object
|
|
||||||
code is in no case prevented or interfered with solely because
|
|
||||||
modification has been made.
|
|
||||||
|
|
||||||
If you convey an object code work under this section in, or with, or
|
|
||||||
specifically for use in, a User Product, and the conveying occurs as
|
|
||||||
part of a transaction in which the right of possession and use of the
|
|
||||||
User Product is transferred to the recipient in perpetuity or for a
|
|
||||||
fixed term (regardless of how the transaction is characterized), the
|
|
||||||
Corresponding Source conveyed under this section must be accompanied
|
|
||||||
by the Installation Information. But this requirement does not apply
|
|
||||||
if neither you nor any third party retains the ability to install
|
|
||||||
modified object code on the User Product (for example, the work has
|
|
||||||
been installed in ROM).
|
|
||||||
|
|
||||||
The requirement to provide Installation Information does not include a
|
|
||||||
requirement to continue to provide support service, warranty, or updates
|
|
||||||
for a work that has been modified or installed by the recipient, or for
|
|
||||||
the User Product in which it has been modified or installed. Access to a
|
|
||||||
network may be denied when the modification itself materially and
|
|
||||||
adversely affects the operation of the network or violates the rules and
|
|
||||||
protocols for communication across the network.
|
|
||||||
|
|
||||||
Corresponding Source conveyed, and Installation Information provided,
|
|
||||||
in accord with this section must be in a format that is publicly
|
|
||||||
documented (and with an implementation available to the public in
|
|
||||||
source code form), and must require no special password or key for
|
|
||||||
unpacking, reading or copying.
|
|
||||||
|
|
||||||
7. Additional Terms.
|
|
||||||
|
|
||||||
"Additional permissions" are terms that supplement the terms of this
|
|
||||||
License by making exceptions from one or more of its conditions.
|
|
||||||
Additional permissions that are applicable to the entire Program shall
|
|
||||||
be treated as though they were included in this License, to the extent
|
|
||||||
that they are valid under applicable law. If additional permissions
|
|
||||||
apply only to part of the Program, that part may be used separately
|
|
||||||
under those permissions, but the entire Program remains governed by
|
|
||||||
this License without regard to the additional permissions.
|
|
||||||
|
|
||||||
When you convey a copy of a covered work, you may at your option
|
|
||||||
remove any additional permissions from that copy, or from any part of
|
|
||||||
it. (Additional permissions may be written to require their own
|
|
||||||
removal in certain cases when you modify the work.) You may place
|
|
||||||
additional permissions on material, added by you to a covered work,
|
|
||||||
for which you have or can give appropriate copyright permission.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, for material you
|
|
||||||
add to a covered work, you may (if authorized by the copyright holders of
|
|
||||||
that material) supplement the terms of this License with terms:
|
|
||||||
|
|
||||||
a) Disclaiming warranty or limiting liability differently from the
|
|
||||||
terms of sections 15 and 16 of this License; or
|
|
||||||
|
|
||||||
b) Requiring preservation of specified reasonable legal notices or
|
|
||||||
author attributions in that material or in the Appropriate Legal
|
|
||||||
Notices displayed by works containing it; or
|
|
||||||
|
|
||||||
c) Prohibiting misrepresentation of the origin of that material, or
|
|
||||||
requiring that modified versions of such material be marked in
|
|
||||||
reasonable ways as different from the original version; or
|
|
||||||
|
|
||||||
d) Limiting the use for publicity purposes of names of licensors or
|
|
||||||
authors of the material; or
|
|
||||||
|
|
||||||
e) Declining to grant rights under trademark law for use of some
|
|
||||||
trade names, trademarks, or service marks; or
|
|
||||||
|
|
||||||
f) Requiring indemnification of licensors and authors of that
|
|
||||||
material by anyone who conveys the material (or modified versions of
|
|
||||||
it) with contractual assumptions of liability to the recipient, for
|
|
||||||
any liability that these contractual assumptions directly impose on
|
|
||||||
those licensors and authors.
|
|
||||||
|
|
||||||
All other non-permissive additional terms are considered "further
|
|
||||||
restrictions" within the meaning of section 10. If the Program as you
|
|
||||||
received it, or any part of it, contains a notice stating that it is
|
|
||||||
governed by this License along with a term that is a further
|
|
||||||
restriction, you may remove that term. If a license document contains
|
|
||||||
a further restriction but permits relicensing or conveying under this
|
|
||||||
License, you may add to a covered work material governed by the terms
|
|
||||||
of that license document, provided that the further restriction does
|
|
||||||
not survive such relicensing or conveying.
|
|
||||||
|
|
||||||
If you add terms to a covered work in accord with this section, you
|
|
||||||
must place, in the relevant source files, a statement of the
|
|
||||||
additional terms that apply to those files, or a notice indicating
|
|
||||||
where to find the applicable terms.
|
|
||||||
|
|
||||||
Additional terms, permissive or non-permissive, may be stated in the
|
|
||||||
form of a separately written license, or stated as exceptions;
|
|
||||||
the above requirements apply either way.
|
|
||||||
|
|
||||||
8. Termination.
|
|
||||||
|
|
||||||
You may not propagate or modify a covered work except as expressly
|
|
||||||
provided under this License. Any attempt otherwise to propagate or
|
|
||||||
modify it is void, and will automatically terminate your rights under
|
|
||||||
this License (including any patent licenses granted under the third
|
|
||||||
paragraph of section 11).
|
|
||||||
|
|
||||||
However, if you cease all violation of this License, then your
|
|
||||||
license from a particular copyright holder is reinstated (a)
|
|
||||||
provisionally, unless and until the copyright holder explicitly and
|
|
||||||
finally terminates your license, and (b) permanently, if the copyright
|
|
||||||
holder fails to notify you of the violation by some reasonable means
|
|
||||||
prior to 60 days after the cessation.
|
|
||||||
|
|
||||||
Moreover, your license from a particular copyright holder is
|
|
||||||
reinstated permanently if the copyright holder notifies you of the
|
|
||||||
violation by some reasonable means, this is the first time you have
|
|
||||||
received notice of violation of this License (for any work) from that
|
|
||||||
copyright holder, and you cure the violation prior to 30 days after
|
|
||||||
your receipt of the notice.
|
|
||||||
|
|
||||||
Termination of your rights under this section does not terminate the
|
|
||||||
licenses of parties who have received copies or rights from you under
|
|
||||||
this License. If your rights have been terminated and not permanently
|
|
||||||
reinstated, you do not qualify to receive new licenses for the same
|
|
||||||
material under section 10.
|
|
||||||
|
|
||||||
9. Acceptance Not Required for Having Copies.
|
|
||||||
|
|
||||||
You are not required to accept this License in order to receive or
|
|
||||||
run a copy of the Program. Ancillary propagation of a covered work
|
|
||||||
occurring solely as a consequence of using peer-to-peer transmission
|
|
||||||
to receive a copy likewise does not require acceptance. However,
|
|
||||||
nothing other than this License grants you permission to propagate or
|
|
||||||
modify any covered work. These actions infringe copyright if you do
|
|
||||||
not accept this License. Therefore, by modifying or propagating a
|
|
||||||
covered work, you indicate your acceptance of this License to do so.
|
|
||||||
|
|
||||||
10. Automatic Licensing of Downstream Recipients.
|
|
||||||
|
|
||||||
Each time you convey a covered work, the recipient automatically
|
|
||||||
receives a license from the original licensors, to run, modify and
|
|
||||||
propagate that work, subject to this License. You are not responsible
|
|
||||||
for enforcing compliance by third parties with this License.
|
|
||||||
|
|
||||||
An "entity transaction" is a transaction transferring control of an
|
|
||||||
organization, or substantially all assets of one, or subdividing an
|
|
||||||
organization, or merging organizations. If propagation of a covered
|
|
||||||
work results from an entity transaction, each party to that
|
|
||||||
transaction who receives a copy of the work also receives whatever
|
|
||||||
licenses to the work the party's predecessor in interest had or could
|
|
||||||
give under the previous paragraph, plus a right to possession of the
|
|
||||||
Corresponding Source of the work from the predecessor in interest, if
|
|
||||||
the predecessor has it or can get it with reasonable efforts.
|
|
||||||
|
|
||||||
You may not impose any further restrictions on the exercise of the
|
|
||||||
rights granted or affirmed under this License. For example, you may
|
|
||||||
not impose a license fee, royalty, or other charge for exercise of
|
|
||||||
rights granted under this License, and you may not initiate litigation
|
|
||||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
|
||||||
any patent claim is infringed by making, using, selling, offering for
|
|
||||||
sale, or importing the Program or any portion of it.
|
|
||||||
|
|
||||||
11. Patents.
|
|
||||||
|
|
||||||
A "contributor" is a copyright holder who authorizes use under this
|
|
||||||
License of the Program or a work on which the Program is based. The
|
|
||||||
work thus licensed is called the contributor's "contributor version".
|
|
||||||
|
|
||||||
A contributor's "essential patent claims" are all patent claims
|
|
||||||
owned or controlled by the contributor, whether already acquired or
|
|
||||||
hereafter acquired, that would be infringed by some manner, permitted
|
|
||||||
by this License, of making, using, or selling its contributor version,
|
|
||||||
but do not include claims that would be infringed only as a
|
|
||||||
consequence of further modification of the contributor version. For
|
|
||||||
purposes of this definition, "control" includes the right to grant
|
|
||||||
patent sublicenses in a manner consistent with the requirements of
|
|
||||||
this License.
|
|
||||||
|
|
||||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
|
||||||
patent license under the contributor's essential patent claims, to
|
|
||||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
|
||||||
propagate the contents of its contributor version.
|
|
||||||
|
|
||||||
In the following three paragraphs, a "patent license" is any express
|
|
||||||
agreement or commitment, however denominated, not to enforce a patent
|
|
||||||
(such as an express permission to practice a patent or covenant not to
|
|
||||||
sue for patent infringement). To "grant" such a patent license to a
|
|
||||||
party means to make such an agreement or commitment not to enforce a
|
|
||||||
patent against the party.
|
|
||||||
|
|
||||||
If you convey a covered work, knowingly relying on a patent license,
|
|
||||||
and the Corresponding Source of the work is not available for anyone
|
|
||||||
to copy, free of charge and under the terms of this License, through a
|
|
||||||
publicly available network server or other readily accessible means,
|
|
||||||
then you must either (1) cause the Corresponding Source to be so
|
|
||||||
available, or (2) arrange to deprive yourself of the benefit of the
|
|
||||||
patent license for this particular work, or (3) arrange, in a manner
|
|
||||||
consistent with the requirements of this License, to extend the patent
|
|
||||||
license to downstream recipients. "Knowingly relying" means you have
|
|
||||||
actual knowledge that, but for the patent license, your conveying the
|
|
||||||
covered work in a country, or your recipient's use of the covered work
|
|
||||||
in a country, would infringe one or more identifiable patents in that
|
|
||||||
country that you have reason to believe are valid.
|
|
||||||
|
|
||||||
If, pursuant to or in connection with a single transaction or
|
|
||||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
|
||||||
covered work, and grant a patent license to some of the parties
|
|
||||||
receiving the covered work authorizing them to use, propagate, modify
|
|
||||||
or convey a specific copy of the covered work, then the patent license
|
|
||||||
you grant is automatically extended to all recipients of the covered
|
|
||||||
work and works based on it.
|
|
||||||
|
|
||||||
A patent license is "discriminatory" if it does not include within
|
|
||||||
the scope of its coverage, prohibits the exercise of, or is
|
|
||||||
conditioned on the non-exercise of one or more of the rights that are
|
|
||||||
specifically granted under this License. You may not convey a covered
|
|
||||||
work if you are a party to an arrangement with a third party that is
|
|
||||||
in the business of distributing software, under which you make payment
|
|
||||||
to the third party based on the extent of your activity of conveying
|
|
||||||
the work, and under which the third party grants, to any of the
|
|
||||||
parties who would receive the covered work from you, a discriminatory
|
|
||||||
patent license (a) in connection with copies of the covered work
|
|
||||||
conveyed by you (or copies made from those copies), or (b) primarily
|
|
||||||
for and in connection with specific products or compilations that
|
|
||||||
contain the covered work, unless you entered into that arrangement,
|
|
||||||
or that patent license was granted, prior to 28 March 2007.
|
|
||||||
|
|
||||||
Nothing in this License shall be construed as excluding or limiting
|
|
||||||
any implied license or other defenses to infringement that may
|
|
||||||
otherwise be available to you under applicable patent law.
|
|
||||||
|
|
||||||
12. No Surrender of Others' Freedom.
|
|
||||||
|
|
||||||
If conditions are imposed on you (whether by court order, agreement or
|
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
|
||||||
excuse you from the conditions of this License. If you cannot convey a
|
|
||||||
covered work so as to satisfy simultaneously your obligations under this
|
|
||||||
License and any other pertinent obligations, then as a consequence you may
|
|
||||||
not convey it at all. For example, if you agree to terms that obligate you
|
|
||||||
to collect a royalty for further conveying from those to whom you convey
|
|
||||||
the Program, the only way you could satisfy both those terms and this
|
|
||||||
License would be to refrain entirely from conveying the Program.
|
|
||||||
|
|
||||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, if you modify the
|
|
||||||
Program, your modified version must prominently offer all users
|
|
||||||
interacting with it remotely through a computer network (if your version
|
|
||||||
supports such interaction) an opportunity to receive the Corresponding
|
|
||||||
Source of your version by providing access to the Corresponding Source
|
|
||||||
from a network server at no charge, through some standard or customary
|
|
||||||
means of facilitating copying of software. This Corresponding Source
|
|
||||||
shall include the Corresponding Source for any work covered by version 3
|
|
||||||
of the GNU General Public License that is incorporated pursuant to the
|
|
||||||
following paragraph.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
|
||||||
permission to link or combine any covered work with a work licensed
|
|
||||||
under version 3 of the GNU General Public License into a single
|
|
||||||
combined work, and to convey the resulting work. The terms of this
|
|
||||||
License will continue to apply to the part which is the covered work,
|
|
||||||
but the work with which it is combined will remain governed by version
|
|
||||||
3 of the GNU General Public License.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
|
||||||
the GNU Affero General Public License from time to time. Such new versions
|
|
||||||
will be similar in spirit to the present version, but may differ in detail to
|
|
||||||
address new problems or concerns.
|
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
|
||||||
Program specifies that a certain numbered version of the GNU Affero General
|
|
||||||
Public License "or any later version" applies to it, you have the
|
|
||||||
option of following the terms and conditions either of that numbered
|
|
||||||
version or of any later version published by the Free Software
|
|
||||||
Foundation. If the Program does not specify a version number of the
|
|
||||||
GNU Affero General Public License, you may choose any version ever published
|
|
||||||
by the Free Software Foundation.
|
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
|
||||||
versions of the GNU Affero General Public License can be used, that proxy's
|
|
||||||
public statement of acceptance of a version permanently authorizes you
|
|
||||||
to choose that version for the Program.
|
|
||||||
|
|
||||||
Later license versions may give you additional or different
|
|
||||||
permissions. However, no additional obligations are imposed on any
|
|
||||||
author or copyright holder as a result of your choosing to follow a
|
|
||||||
later version.
|
|
||||||
|
|
||||||
15. Disclaimer of Warranty.
|
|
||||||
|
|
||||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
|
||||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
|
||||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
|
||||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
||||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
|
||||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
|
||||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
|
||||||
|
|
||||||
16. Limitation of Liability.
|
|
||||||
|
|
||||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
|
||||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
|
||||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
|
||||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
|
||||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
|
||||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
|
||||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
|
||||||
SUCH DAMAGES.
|
|
||||||
|
|
||||||
17. Interpretation of Sections 15 and 16.
|
|
||||||
|
|
||||||
If the disclaimer of warranty and limitation of liability provided
|
|
||||||
above cannot be given local legal effect according to their terms,
|
|
||||||
reviewing courts shall apply local law that most closely approximates
|
|
||||||
an absolute waiver of all civil liability in connection with the
|
|
||||||
Program, unless a warranty or assumption of liability accompanies a
|
|
||||||
copy of the Program in return for a fee.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
|
||||||
possible use to the public, the best way to achieve this is to make it
|
|
||||||
free software which everyone can redistribute and change under these terms.
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest
|
|
||||||
to attach them to the start of each source file to most effectively
|
|
||||||
state the exclusion of warranty; and each file should have at least
|
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
|
||||||
Copyright (C) <year> <name of author>
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published
|
|
||||||
by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If your software can interact with users remotely through a computer
|
|
||||||
network, you should also make sure that it provides a way for users to
|
|
||||||
get its source. For example, if your program is a web application, its
|
|
||||||
interface could display a "Source" link that leads users to an archive
|
|
||||||
of the code. There are many ways you could offer source, and different
|
|
||||||
solutions will be better for different programs; see section 13 for the
|
|
||||||
specific requirements.
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
|
||||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
|
||||||
<https://www.gnu.org/licenses/>.
|
|
24
LICENSE.md
Normal file
24
LICENSE.md
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
This is free and unencumbered software released into the public domain.
|
||||||
|
|
||||||
|
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||||
|
distribute this software, either in source code form or as a compiled
|
||||||
|
binary, for any purpose, commercial or non-commercial, and by any
|
||||||
|
means.
|
||||||
|
|
||||||
|
In jurisdictions that recognize copyright laws, the author or authors
|
||||||
|
of this software dedicate any and all copyright interest in the
|
||||||
|
software to the public domain. We make this dedication for the benefit
|
||||||
|
of the public at large and to the detriment of our heirs and
|
||||||
|
successors. We intend this dedication to be an overt act of
|
||||||
|
relinquishment in perpetuity of all present and future rights to this
|
||||||
|
software under copyright law.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||||
|
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||||
|
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
|
OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
For more information, please refer to <http://unlicense.org>
|
150
README.md
150
README.md
|
@ -2,107 +2,75 @@
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
**Munera** is your go-to companion for managing expenses efficiently and effortlessly, whether you're tracking daily expenditures, managing recurring expenses, or keeping tabs on creditors and debtors.
|
Munera is a companion for managing expenses efficiently and effortlessly, whether you're tracking daily expenditures, managing recurring expenses, or keeping tabs on creditors and debtors.
|
||||||
|
|
||||||
Munera is built using **Java** with the **Spring** framework. It utilizes **Vaadin Flow** for creating modern web applications. The application uses a **PostgreSQL** database for data storage. **Maven** is employed as the build tool, while **JUnit** and **Mockito** are used for unit testing. **Spring Security** handles authentication, and **Apache Commons** to export data.
|
|
||||||
|
|
||||||
### Dashboard
|
|
||||||
|
|
||||||
![Dashboard](src/main/resources/pictures/dashboard.png)
|
![Dashboard](src/main/resources/pictures/dashboard.png)
|
||||||
|
![Grid](src/main/resources/pictures/grid.png)
|
||||||
### Expenses Grid
|
|
||||||
|
|
||||||
![Expenses Grid](src/main/resources/pictures/grid.png)
|
|
||||||
|
|
||||||
### People Grid
|
|
||||||
|
|
||||||
![People Grid](src/main/resources/pictures/people.png)
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
### 1. Expense Management
|
### 1. Expense Management
|
||||||
|
|
||||||
Easily create, read, update, and delete expenses with comprehensive details, including:
|
- Create, read, update, and delete expenses with the following details:
|
||||||
|
- Name
|
||||||
|
- Date
|
||||||
|
- Category
|
||||||
|
- Cost
|
||||||
|
- Description
|
||||||
|
- Period Interval (1, 2, 3, etc)
|
||||||
|
- Period Unit (days, weeks, months)
|
||||||
|
- Creditors and debtors
|
||||||
|
- Status
|
||||||
|
-
|
||||||
|
- Create, read, update and delete categories
|
||||||
|
- Create, read, update and delete people
|
||||||
|
- Create, read, update and delete events
|
||||||
|
|
||||||
- **Name**: The name of the expense.
|
## Completed Features
|
||||||
- **Date**: The date the expense occurred.
|
- [x] Create categories with CRUD operations
|
||||||
- **Category**: Assign categories to expenses for easier tracking.
|
- [x] Create concept of creditors and debtors for each expense
|
||||||
- **Cost**: The amount spent.
|
- [x] Insert expenses of the past and of the future
|
||||||
- **Description**: Additional notes or details about the expense.
|
- [x] Have an option to set up recurring expenses
|
||||||
- **Period Interval**: Set the recurrence interval (e.g., 1, 2, 3, etc.).
|
- [x] Creditors and debtors total expenses
|
||||||
- **Period Unit**: Specify the unit for the period interval (days, weeks, months).
|
- [x] Setup status for each expense
|
||||||
- **Payer and Beneficiary**: Record who paid and who benefited from the expense.
|
- [x] Create mechanism to change status when expense gets paid
|
||||||
- **Date of Payment**: The date when the payment was made.
|
- [x] Create labels to show if a person is in debit or credit with you
|
||||||
- **Status of Expense**: Track the status of the expense (e.g., "Paid to Me", "Owed to Me", "Paid by Me", "Owed by Me").
|
- [x] First tries with widgets in a dashboard view
|
||||||
|
- [x] An expense cant have the same person as both creditor and debtors
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
### 2. Category Management
|
1. **Filtering and Sorting**
|
||||||
|
- Keep implementing sorting on more columns, start implementing filtering
|
||||||
|
- Specification
|
||||||
|
|
||||||
|
2. **Weekly and Monthly Summaries**
|
||||||
|
- Create functionality to generate weekly and monthly summaries, including filtering and sorting options.
|
||||||
|
- ~~Create a dashboard or log of "next expenses" that lists the next recurring expenses that you expect to receive~~
|
||||||
|
|
||||||
- Manage categories for better organization of expenses.
|
3. **Reports for Creditors and Debtors**
|
||||||
- Default categories are initialized on the first build of the application.
|
- ~~Develop reports outlining debts or credits for each creditor and debtor to provide users with a comprehensive overview.~~
|
||||||
|
- ~~CRUD operations for creditors and debtors~~
|
||||||
|
|
||||||
### 3. People Management
|
4. **Create a way to set the currency of each expense**
|
||||||
|
- It should be possible in the form of an expense
|
||||||
|
- The symbol should be added to the cost column
|
||||||
|
- If possible, the calculations should take into account the different currencies
|
||||||
|
|
||||||
- Track expenses related to people and generate reports on amounts owed or owing.
|
5. **Events**
|
||||||
|
- ~~Options to create events in which to put expenses (vacations, congress, etc)~~
|
||||||
|
- ~~Each event has a number of people connected to it, expenses can be added to these people~~
|
||||||
|
- A reports tells which people need to give/take money to which people in order to be even
|
||||||
|
|
||||||
### 4. Reporting and Analysis
|
6. **Misc**
|
||||||
|
- PeriodUnit and Interval need to be implemented with a scheduler
|
||||||
|
- Graphs could use more work, maybe some filtering?
|
||||||
|
- More validation in form
|
||||||
|
- ~~Login page~~
|
||||||
|
- email setup
|
||||||
|
- Migration tool for DB changes in prod
|
||||||
|
|
||||||
- Generate reports to view expenses and debts by category or person.
|
## Known Issues
|
||||||
- Visualize your expenses with built-in graphs.
|
|
||||||
|
|
||||||
### 5. Data Export
|
- PeopleVIew dosent refresh after an edit operation anymore
|
||||||
|
- Form still needs more validation when empty, some entities can be created with all null values, even the ones that have constraints throw SQL errors, they need to be gracefully handled.
|
||||||
- Export your expense data to CSV format for external use and backup.
|
- Errors need to be caught and handled
|
||||||
|
- Graphs still need ~~a lot of~~ **some** improvements
|
||||||
### 6. Upcoming Features
|
- ~~ExpenseView dosent refresh after an edit operation anymore~~
|
||||||
|
|
||||||
- **Income Tracking**: An upcoming feature to manage and track income efficiently.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
You can self-host Munera in two ways:
|
|
||||||
|
|
||||||
### Option 1: Manual Installation
|
|
||||||
|
|
||||||
1. **Compile and Run**: Download the source code, compile, and run the application manually with your preferred SQL database.
|
|
||||||
2. **Default Setup**: The first build will automatically create default categories and an admin user, as specified in the `application.properties` file.
|
|
||||||
|
|
||||||
### Option 2: Docker Installation
|
|
||||||
|
|
||||||
1. **Dockerized Setup**: Use the provided `Dockerfile` and `docker-compose.yml` files to deploy Munera using Docker.
|
|
||||||
2. **Configuration**: The first build will set up default categories and an admin user based on the environment variables in `docker-compose.yml`.
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
Follow the steps below to get started with Munera:
|
|
||||||
|
|
||||||
1. **Clone the Repository**:
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/yourusername/munera.git
|
|
||||||
cd munera
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Manual Installation**:
|
|
||||||
|
|
||||||
- Configure your SQL database.
|
|
||||||
- Adjust settings in `application.properties`.
|
|
||||||
- Build and run the application with Maven:
|
|
||||||
```bash
|
|
||||||
mvn clean install
|
|
||||||
mvn spring-boot:run
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Docker Installation**:
|
|
||||||
|
|
||||||
- Ensure Docker is installed and running.
|
|
||||||
- Use Docker Compose to set up Munera:
|
|
||||||
```bash
|
|
||||||
docker-compose up --build
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
Munera is licensed under the **GNU Affero General Public License** (AGPL) Version 3. See the LICENSE file for details.
|
|
||||||
|
|
||||||
## Feedback and Contributions
|
|
||||||
|
|
||||||
Contributions and feedback are welcome! Feel free to submit a pull request or open an issue on the [GitHub repository](https://github.com/filippo-ferrari/munera).
|
|
|
@ -1,34 +0,0 @@
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
vaadin-app:
|
|
||||||
image: vaadin-app
|
|
||||||
build: .
|
|
||||||
ports:
|
|
||||||
- "8080:8080"
|
|
||||||
environment:
|
|
||||||
SPRING_DATASOURCE_URL: ${DB_ADDRESS}/${DB_NAME}
|
|
||||||
SPRING_DATASOURCE_USERNAME: ${DB_USER}
|
|
||||||
SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD}
|
|
||||||
ADMIN_USERNAME: ${ADMIN_USERNAME}
|
|
||||||
ADMIN_PASSWORD: ${ADMIN_PASSWORD}
|
|
||||||
ADMIN_FIRST_NAME: ${ADMIN_FIRST_NAME}
|
|
||||||
ADMIN_LAST_NAME: ${ADMIN_LAST_NAME}
|
|
||||||
ADMIN_ROLES: ${ADMIN_ROLES}
|
|
||||||
ADMIN_EMAIL: ${ADMIN_EMAIL}
|
|
||||||
depends_on:
|
|
||||||
- db
|
|
||||||
|
|
||||||
db:
|
|
||||||
image: postgres:13
|
|
||||||
environment:
|
|
||||||
POSTGRES_DB: ${DB_NAME}
|
|
||||||
POSTGRES_USER: ${DB_USER}
|
|
||||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
|
||||||
ports:
|
|
||||||
- "5432:5432"
|
|
||||||
volumes:
|
|
||||||
- db_data:/var/lib/postgresql/data
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
db_data:
|
|
15654
package-lock.json
generated
15654
package-lock.json
generated
File diff suppressed because it is too large
Load diff
290
package.json
290
package.json
|
@ -1,290 +0,0 @@
|
||||||
{
|
|
||||||
"name": "no-name",
|
|
||||||
"license": "UNLICENSED",
|
|
||||||
"type": "module",
|
|
||||||
"dependencies": {
|
|
||||||
"@polymer/iron-iconset-svg": "^3.0.1",
|
|
||||||
"@polymer/paper-button": "^3.0.1",
|
|
||||||
"@polymer/paper-icon-button": "^3.0.2",
|
|
||||||
"@polymer/polymer": "3.5.1",
|
|
||||||
"@vaadin/a11y-base": "24.3.13",
|
|
||||||
"@vaadin/accordion": "24.3.13",
|
|
||||||
"@vaadin/app-layout": "24.3.13",
|
|
||||||
"@vaadin/avatar": "24.3.13",
|
|
||||||
"@vaadin/avatar-group": "24.3.13",
|
|
||||||
"@vaadin/board": "24.3.13",
|
|
||||||
"@vaadin/bundles": "24.3.13",
|
|
||||||
"@vaadin/button": "24.3.13",
|
|
||||||
"@vaadin/charts": "24.3.13",
|
|
||||||
"@vaadin/checkbox": "24.3.13",
|
|
||||||
"@vaadin/checkbox-group": "24.3.13",
|
|
||||||
"@vaadin/combo-box": "24.3.13",
|
|
||||||
"@vaadin/common-frontend": "0.0.19",
|
|
||||||
"@vaadin/component-base": "24.3.13",
|
|
||||||
"@vaadin/confirm-dialog": "24.3.13",
|
|
||||||
"@vaadin/context-menu": "24.3.13",
|
|
||||||
"@vaadin/cookie-consent": "24.3.13",
|
|
||||||
"@vaadin/crud": "24.3.13",
|
|
||||||
"@vaadin/custom-field": "24.3.13",
|
|
||||||
"@vaadin/date-picker": "24.3.13",
|
|
||||||
"@vaadin/date-time-picker": "24.3.13",
|
|
||||||
"@vaadin/details": "24.3.13",
|
|
||||||
"@vaadin/dialog": "24.3.13",
|
|
||||||
"@vaadin/email-field": "24.3.13",
|
|
||||||
"@vaadin/field-base": "24.3.13",
|
|
||||||
"@vaadin/field-highlighter": "24.3.13",
|
|
||||||
"@vaadin/form-layout": "24.3.13",
|
|
||||||
"@vaadin/grid": "24.3.13",
|
|
||||||
"@vaadin/grid-pro": "24.3.13",
|
|
||||||
"@vaadin/horizontal-layout": "24.3.13",
|
|
||||||
"@vaadin/icon": "24.3.13",
|
|
||||||
"@vaadin/icons": "24.3.13",
|
|
||||||
"@vaadin/input-container": "24.3.13",
|
|
||||||
"@vaadin/integer-field": "24.3.13",
|
|
||||||
"@vaadin/item": "24.3.13",
|
|
||||||
"@vaadin/list-box": "24.3.13",
|
|
||||||
"@vaadin/lit-renderer": "24.3.13",
|
|
||||||
"@vaadin/login": "24.3.13",
|
|
||||||
"@vaadin/map": "24.3.13",
|
|
||||||
"@vaadin/menu-bar": "24.3.13",
|
|
||||||
"@vaadin/message-input": "24.3.13",
|
|
||||||
"@vaadin/message-list": "24.3.13",
|
|
||||||
"@vaadin/multi-select-combo-box": "24.3.13",
|
|
||||||
"@vaadin/notification": "24.3.13",
|
|
||||||
"@vaadin/number-field": "24.3.13",
|
|
||||||
"@vaadin/overlay": "24.3.13",
|
|
||||||
"@vaadin/password-field": "24.3.13",
|
|
||||||
"@vaadin/polymer-legacy-adapter": "24.3.13",
|
|
||||||
"@vaadin/progress-bar": "24.3.13",
|
|
||||||
"@vaadin/radio-group": "24.3.13",
|
|
||||||
"@vaadin/rich-text-editor": "24.3.13",
|
|
||||||
"@vaadin/router": "1.7.5",
|
|
||||||
"@vaadin/scroller": "24.3.13",
|
|
||||||
"@vaadin/select": "24.3.13",
|
|
||||||
"@vaadin/side-nav": "24.3.13",
|
|
||||||
"@vaadin/split-layout": "24.3.13",
|
|
||||||
"@vaadin/tabs": "24.3.13",
|
|
||||||
"@vaadin/tabsheet": "24.3.13",
|
|
||||||
"@vaadin/text-area": "24.3.13",
|
|
||||||
"@vaadin/text-field": "24.3.13",
|
|
||||||
"@vaadin/time-picker": "24.3.13",
|
|
||||||
"@vaadin/tooltip": "24.3.13",
|
|
||||||
"@vaadin/upload": "24.3.13",
|
|
||||||
"@vaadin/vaadin-development-mode-detector": "2.0.7",
|
|
||||||
"@vaadin/vaadin-lumo-styles": "24.3.13",
|
|
||||||
"@vaadin/vaadin-material-styles": "24.3.13",
|
|
||||||
"@vaadin/vaadin-themable-mixin": "24.3.13",
|
|
||||||
"@vaadin/vaadin-usage-statistics": "2.1.2",
|
|
||||||
"@vaadin/vertical-layout": "24.3.13",
|
|
||||||
"@vaadin/virtual-list": "24.3.13",
|
|
||||||
"construct-style-sheets-polyfill": "3.1.0",
|
|
||||||
"date-fns": "2.29.3",
|
|
||||||
"lit": "3.1.2",
|
|
||||||
"lit-element": "^3.2.2",
|
|
||||||
"lit-html": "2.4.0",
|
|
||||||
"proj4": "2.9.2"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@rollup/plugin-replace": "5.0.5",
|
|
||||||
"@rollup/pluginutils": "5.1.0",
|
|
||||||
"@vitejs/plugin-react": "4.2.1",
|
|
||||||
"@vitejs/plugin-react-swc": "3.5.0",
|
|
||||||
"async": "3.2.4",
|
|
||||||
"glob": "10.3.3",
|
|
||||||
"rollup-plugin-brotli": "3.1.0",
|
|
||||||
"rollup-plugin-visualizer": "5.12.0",
|
|
||||||
"strip-css-comments": "5.0.0",
|
|
||||||
"transform-ast": "2.4.4",
|
|
||||||
"typescript": "5.3.3",
|
|
||||||
"vite": "5.1.7",
|
|
||||||
"vite-plugin-checker": "0.6.4",
|
|
||||||
"workbox-build": "7.0.0",
|
|
||||||
"workbox-core": "7.0.0",
|
|
||||||
"workbox-precaching": "7.0.0"
|
|
||||||
},
|
|
||||||
"vaadin": {
|
|
||||||
"dependencies": {
|
|
||||||
"@polymer/iron-iconset-svg": "^3.0.1",
|
|
||||||
"@polymer/paper-button": "^3.0.1",
|
|
||||||
"@polymer/paper-icon-button": "^3.0.2",
|
|
||||||
"@polymer/polymer": "3.5.1",
|
|
||||||
"@vaadin/a11y-base": "24.3.13",
|
|
||||||
"@vaadin/accordion": "24.3.13",
|
|
||||||
"@vaadin/app-layout": "24.3.13",
|
|
||||||
"@vaadin/avatar": "24.3.13",
|
|
||||||
"@vaadin/avatar-group": "24.3.13",
|
|
||||||
"@vaadin/board": "24.3.13",
|
|
||||||
"@vaadin/bundles": "24.3.13",
|
|
||||||
"@vaadin/button": "24.3.13",
|
|
||||||
"@vaadin/charts": "24.3.13",
|
|
||||||
"@vaadin/checkbox": "24.3.13",
|
|
||||||
"@vaadin/checkbox-group": "24.3.13",
|
|
||||||
"@vaadin/combo-box": "24.3.13",
|
|
||||||
"@vaadin/common-frontend": "0.0.19",
|
|
||||||
"@vaadin/component-base": "24.3.13",
|
|
||||||
"@vaadin/confirm-dialog": "24.3.13",
|
|
||||||
"@vaadin/context-menu": "24.3.13",
|
|
||||||
"@vaadin/cookie-consent": "24.3.13",
|
|
||||||
"@vaadin/crud": "24.3.13",
|
|
||||||
"@vaadin/custom-field": "24.3.13",
|
|
||||||
"@vaadin/date-picker": "24.3.13",
|
|
||||||
"@vaadin/date-time-picker": "24.3.13",
|
|
||||||
"@vaadin/details": "24.3.13",
|
|
||||||
"@vaadin/dialog": "24.3.13",
|
|
||||||
"@vaadin/email-field": "24.3.13",
|
|
||||||
"@vaadin/field-base": "24.3.13",
|
|
||||||
"@vaadin/field-highlighter": "24.3.13",
|
|
||||||
"@vaadin/form-layout": "24.3.13",
|
|
||||||
"@vaadin/grid": "24.3.13",
|
|
||||||
"@vaadin/grid-pro": "24.3.13",
|
|
||||||
"@vaadin/horizontal-layout": "24.3.13",
|
|
||||||
"@vaadin/icon": "24.3.13",
|
|
||||||
"@vaadin/icons": "24.3.13",
|
|
||||||
"@vaadin/input-container": "24.3.13",
|
|
||||||
"@vaadin/integer-field": "24.3.13",
|
|
||||||
"@vaadin/item": "24.3.13",
|
|
||||||
"@vaadin/list-box": "24.3.13",
|
|
||||||
"@vaadin/lit-renderer": "24.3.13",
|
|
||||||
"@vaadin/login": "24.3.13",
|
|
||||||
"@vaadin/map": "24.3.13",
|
|
||||||
"@vaadin/menu-bar": "24.3.13",
|
|
||||||
"@vaadin/message-input": "24.3.13",
|
|
||||||
"@vaadin/message-list": "24.3.13",
|
|
||||||
"@vaadin/multi-select-combo-box": "24.3.13",
|
|
||||||
"@vaadin/notification": "24.3.13",
|
|
||||||
"@vaadin/number-field": "24.3.13",
|
|
||||||
"@vaadin/overlay": "24.3.13",
|
|
||||||
"@vaadin/password-field": "24.3.13",
|
|
||||||
"@vaadin/polymer-legacy-adapter": "24.3.13",
|
|
||||||
"@vaadin/progress-bar": "24.3.13",
|
|
||||||
"@vaadin/radio-group": "24.3.13",
|
|
||||||
"@vaadin/rich-text-editor": "24.3.13",
|
|
||||||
"@vaadin/router": "1.7.5",
|
|
||||||
"@vaadin/scroller": "24.3.13",
|
|
||||||
"@vaadin/select": "24.3.13",
|
|
||||||
"@vaadin/side-nav": "24.3.13",
|
|
||||||
"@vaadin/split-layout": "24.3.13",
|
|
||||||
"@vaadin/tabs": "24.3.13",
|
|
||||||
"@vaadin/tabsheet": "24.3.13",
|
|
||||||
"@vaadin/text-area": "24.3.13",
|
|
||||||
"@vaadin/text-field": "24.3.13",
|
|
||||||
"@vaadin/time-picker": "24.3.13",
|
|
||||||
"@vaadin/tooltip": "24.3.13",
|
|
||||||
"@vaadin/upload": "24.3.13",
|
|
||||||
"@vaadin/vaadin-development-mode-detector": "2.0.7",
|
|
||||||
"@vaadin/vaadin-lumo-styles": "24.3.13",
|
|
||||||
"@vaadin/vaadin-material-styles": "24.3.13",
|
|
||||||
"@vaadin/vaadin-themable-mixin": "24.3.13",
|
|
||||||
"@vaadin/vaadin-usage-statistics": "2.1.2",
|
|
||||||
"@vaadin/vertical-layout": "24.3.13",
|
|
||||||
"@vaadin/virtual-list": "24.3.13",
|
|
||||||
"construct-style-sheets-polyfill": "3.1.0",
|
|
||||||
"date-fns": "2.29.3",
|
|
||||||
"lit": "3.1.2",
|
|
||||||
"lit-element": "^3.2.2",
|
|
||||||
"lit-html": "2.4.0",
|
|
||||||
"proj4": "2.9.2"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@rollup/plugin-replace": "5.0.5",
|
|
||||||
"@rollup/pluginutils": "5.1.0",
|
|
||||||
"@vitejs/plugin-react": "4.2.1",
|
|
||||||
"@vitejs/plugin-react-swc": "3.5.0",
|
|
||||||
"async": "3.2.4",
|
|
||||||
"glob": "10.3.3",
|
|
||||||
"rollup-plugin-brotli": "3.1.0",
|
|
||||||
"rollup-plugin-visualizer": "5.12.0",
|
|
||||||
"strip-css-comments": "5.0.0",
|
|
||||||
"transform-ast": "2.4.4",
|
|
||||||
"typescript": "5.3.3",
|
|
||||||
"vite": "5.1.7",
|
|
||||||
"vite-plugin-checker": "0.6.4",
|
|
||||||
"workbox-build": "7.0.0",
|
|
||||||
"workbox-core": "7.0.0",
|
|
||||||
"workbox-precaching": "7.0.0"
|
|
||||||
},
|
|
||||||
"hash": "2b6c6cf25413540c35c7b6c7e348be146ae524e27f18cdc7249a709d05dee8c9"
|
|
||||||
},
|
|
||||||
"overrides": {
|
|
||||||
"@vaadin/bundles": "$@vaadin/bundles",
|
|
||||||
"@vaadin/a11y-base": "$@vaadin/a11y-base",
|
|
||||||
"@vaadin/accordion": "$@vaadin/accordion",
|
|
||||||
"@vaadin/app-layout": "$@vaadin/app-layout",
|
|
||||||
"@vaadin/avatar": "$@vaadin/avatar",
|
|
||||||
"@vaadin/avatar-group": "$@vaadin/avatar-group",
|
|
||||||
"@vaadin/button": "$@vaadin/button",
|
|
||||||
"@vaadin/checkbox": "$@vaadin/checkbox",
|
|
||||||
"@vaadin/checkbox-group": "$@vaadin/checkbox-group",
|
|
||||||
"@vaadin/combo-box": "$@vaadin/combo-box",
|
|
||||||
"@vaadin/component-base": "$@vaadin/component-base",
|
|
||||||
"@vaadin/confirm-dialog": "$@vaadin/confirm-dialog",
|
|
||||||
"@vaadin/context-menu": "$@vaadin/context-menu",
|
|
||||||
"@vaadin/custom-field": "$@vaadin/custom-field",
|
|
||||||
"@vaadin/date-picker": "$@vaadin/date-picker",
|
|
||||||
"@vaadin/date-time-picker": "$@vaadin/date-time-picker",
|
|
||||||
"@vaadin/details": "$@vaadin/details",
|
|
||||||
"@vaadin/dialog": "$@vaadin/dialog",
|
|
||||||
"@vaadin/email-field": "$@vaadin/email-field",
|
|
||||||
"@vaadin/field-base": "$@vaadin/field-base",
|
|
||||||
"@vaadin/field-highlighter": "$@vaadin/field-highlighter",
|
|
||||||
"@vaadin/form-layout": "$@vaadin/form-layout",
|
|
||||||
"@vaadin/grid": "$@vaadin/grid",
|
|
||||||
"@vaadin/horizontal-layout": "$@vaadin/horizontal-layout",
|
|
||||||
"@vaadin/icon": "$@vaadin/icon",
|
|
||||||
"@vaadin/icons": "$@vaadin/icons",
|
|
||||||
"@vaadin/input-container": "$@vaadin/input-container",
|
|
||||||
"@vaadin/integer-field": "$@vaadin/integer-field",
|
|
||||||
"@vaadin/item": "$@vaadin/item",
|
|
||||||
"@vaadin/list-box": "$@vaadin/list-box",
|
|
||||||
"@vaadin/lit-renderer": "$@vaadin/lit-renderer",
|
|
||||||
"@vaadin/login": "$@vaadin/login",
|
|
||||||
"@vaadin/menu-bar": "$@vaadin/menu-bar",
|
|
||||||
"@vaadin/message-input": "$@vaadin/message-input",
|
|
||||||
"@vaadin/message-list": "$@vaadin/message-list",
|
|
||||||
"@vaadin/multi-select-combo-box": "$@vaadin/multi-select-combo-box",
|
|
||||||
"@vaadin/notification": "$@vaadin/notification",
|
|
||||||
"@vaadin/number-field": "$@vaadin/number-field",
|
|
||||||
"@vaadin/overlay": "$@vaadin/overlay",
|
|
||||||
"@vaadin/password-field": "$@vaadin/password-field",
|
|
||||||
"@vaadin/polymer-legacy-adapter": "$@vaadin/polymer-legacy-adapter",
|
|
||||||
"@vaadin/progress-bar": "$@vaadin/progress-bar",
|
|
||||||
"@vaadin/radio-group": "$@vaadin/radio-group",
|
|
||||||
"@vaadin/scroller": "$@vaadin/scroller",
|
|
||||||
"@vaadin/select": "$@vaadin/select",
|
|
||||||
"@vaadin/side-nav": "$@vaadin/side-nav",
|
|
||||||
"@vaadin/split-layout": "$@vaadin/split-layout",
|
|
||||||
"@vaadin/tabs": "$@vaadin/tabs",
|
|
||||||
"@vaadin/tabsheet": "$@vaadin/tabsheet",
|
|
||||||
"@vaadin/text-area": "$@vaadin/text-area",
|
|
||||||
"@vaadin/text-field": "$@vaadin/text-field",
|
|
||||||
"@vaadin/time-picker": "$@vaadin/time-picker",
|
|
||||||
"@vaadin/tooltip": "$@vaadin/tooltip",
|
|
||||||
"@vaadin/upload": "$@vaadin/upload",
|
|
||||||
"@vaadin/vaadin-development-mode-detector": "$@vaadin/vaadin-development-mode-detector",
|
|
||||||
"@vaadin/vaadin-lumo-styles": "$@vaadin/vaadin-lumo-styles",
|
|
||||||
"@vaadin/vaadin-material-styles": "$@vaadin/vaadin-material-styles",
|
|
||||||
"@vaadin/router": "$@vaadin/router",
|
|
||||||
"@vaadin/vaadin-usage-statistics": "$@vaadin/vaadin-usage-statistics",
|
|
||||||
"@vaadin/vertical-layout": "$@vaadin/vertical-layout",
|
|
||||||
"@vaadin/virtual-list": "$@vaadin/virtual-list",
|
|
||||||
"@vaadin/board": "$@vaadin/board",
|
|
||||||
"@vaadin/charts": "$@vaadin/charts",
|
|
||||||
"@vaadin/cookie-consent": "$@vaadin/cookie-consent",
|
|
||||||
"@vaadin/crud": "$@vaadin/crud",
|
|
||||||
"@vaadin/grid-pro": "$@vaadin/grid-pro",
|
|
||||||
"@vaadin/map": "$@vaadin/map",
|
|
||||||
"@vaadin/rich-text-editor": "$@vaadin/rich-text-editor",
|
|
||||||
"@vaadin/common-frontend": "$@vaadin/common-frontend",
|
|
||||||
"construct-style-sheets-polyfill": "$construct-style-sheets-polyfill",
|
|
||||||
"lit": "$lit",
|
|
||||||
"@polymer/polymer": "$@polymer/polymer",
|
|
||||||
"proj4": "$proj4",
|
|
||||||
"@vaadin/vaadin-themable-mixin": "$@vaadin/vaadin-themable-mixin",
|
|
||||||
"date-fns": "$date-fns",
|
|
||||||
"@polymer/iron-iconset-svg": "$@polymer/iron-iconset-svg",
|
|
||||||
"lit-html": "$lit-html",
|
|
||||||
"@polymer/paper-icon-button": "$@polymer/paper-icon-button",
|
|
||||||
"lit-element": "$lit-element",
|
|
||||||
"@polymer/paper-button": "$@polymer/paper-button"
|
|
||||||
}
|
|
||||||
}
|
|
6
pom.xml
6
pom.xml
|
@ -52,12 +52,6 @@
|
||||||
<groupId>com.vaadin</groupId>
|
<groupId>com.vaadin</groupId>
|
||||||
<artifactId>vaadin-spring-boot-starter</artifactId>
|
<artifactId>vaadin-spring-boot-starter</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-csv -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.apache.commons</groupId>
|
|
||||||
<artifactId>commons-csv</artifactId>
|
|
||||||
<version>1.11.0</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.vaadin.klaudeta</groupId>
|
<groupId>org.vaadin.klaudeta</groupId>
|
||||||
<artifactId>grid-pagination</artifactId>
|
<artifactId>grid-pagination</artifactId>
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
This directory is automatically generated by Vaadin and contains the pre-compiled
|
|
||||||
frontend files/resources for your project (frontend development bundle).
|
|
||||||
|
|
||||||
It should be added to Version Control System and committed, so that other developers
|
|
||||||
do not have to compile it again.
|
|
||||||
|
|
||||||
Frontend development bundle is automatically updated when needed:
|
|
||||||
- an npm/pnpm package is added with @NpmPackage or directly into package.json
|
|
||||||
- CSS, JavaScript or TypeScript files are added with @CssImport, @JsModule or @JavaScript
|
|
||||||
- Vaadin add-on with front-end customizations is added
|
|
||||||
- Custom theme imports/assets added into 'theme.json' file
|
|
||||||
- Exported web component is added.
|
|
||||||
|
|
||||||
If your project development needs a hot deployment of the frontend changes,
|
|
||||||
you can switch Flow to use Vite development server (default in Vaadin 23.3 and earlier versions):
|
|
||||||
- set `vaadin.frontend.hotdeploy=true` in `application.properties`
|
|
||||||
- configure `vaadin-maven-plugin`:
|
|
||||||
```
|
|
||||||
<configuration>
|
|
||||||
<frontendHotdeploy>true</frontendHotdeploy>
|
|
||||||
</configuration>
|
|
||||||
```
|
|
||||||
- configure `jetty-maven-plugin`:
|
|
||||||
```
|
|
||||||
<configuration>
|
|
||||||
<systemProperties>
|
|
||||||
<vaadin.frontend.hotdeploy>true</vaadin.frontend.hotdeploy>
|
|
||||||
</systemProperties>
|
|
||||||
</configuration>
|
|
||||||
```
|
|
||||||
|
|
||||||
Read more [about Vaadin development mode](https://vaadin.com/docs/next/configuration/development-mode/#pre-compiled-front-end-bundle-for-faster-start-up).
|
|
Binary file not shown.
Binary file not shown.
|
@ -3,14 +3,13 @@ package com.application.munera;
|
||||||
import com.application.munera.repositories.ExpenseRepository;
|
import com.application.munera.repositories.ExpenseRepository;
|
||||||
import com.vaadin.flow.component.page.AppShellConfigurator;
|
import com.vaadin.flow.component.page.AppShellConfigurator;
|
||||||
import com.vaadin.flow.theme.Theme;
|
import com.vaadin.flow.theme.Theme;
|
||||||
|
import javax.sql.DataSource;
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.boot.autoconfigure.sql.init.SqlDataSourceScriptDatabaseInitializer;
|
import org.springframework.boot.autoconfigure.sql.init.SqlDataSourceScriptDatabaseInitializer;
|
||||||
import org.springframework.boot.autoconfigure.sql.init.SqlInitializationProperties;
|
import org.springframework.boot.autoconfigure.sql.init.SqlInitializationProperties;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
|
|
||||||
import javax.sql.DataSource;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The entry point of the Spring Boot application.
|
* The entry point of the Spring Boot application.
|
||||||
*
|
*
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
package com.application.munera;
|
||||||
|
|
||||||
|
import com.application.munera.views.login.LoginView;
|
||||||
|
import com.vaadin.flow.spring.security.VaadinWebSecurity;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.WebSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.core.userdetails.User;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||||
|
import org.springframework.security.provisioning.UserDetailsManager;
|
||||||
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||||
|
|
||||||
|
@EnableWebSecurity
|
||||||
|
@Configuration
|
||||||
|
public class SecurityConfiguration
|
||||||
|
extends VaadinWebSecurity {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void configure(HttpSecurity http) throws Exception {
|
||||||
|
// Delegating the responsibility of general configurations
|
||||||
|
// of http security to the super class. It's configuring
|
||||||
|
// the followings: Vaadin's CSRF protection by ignoring
|
||||||
|
// framework's internal requests, default request cache,
|
||||||
|
// ignoring public views annotated with @AnonymousAllowed,
|
||||||
|
// restricting access to other views/endpoints, and enabling
|
||||||
|
// NavigationAccessControl authorization.
|
||||||
|
// You can add any possible extra configurations of your own
|
||||||
|
// here (the following is just an example):
|
||||||
|
|
||||||
|
// http.rememberMe().alwaysRemember(false);
|
||||||
|
|
||||||
|
// Configure your static resources with public access before calling
|
||||||
|
// super.configure(HttpSecurity) as it adds final anyRequest matcher
|
||||||
|
http.authorizeHttpRequests(auth -> auth.requestMatchers(new AntPathRequestMatcher("/public/**"))
|
||||||
|
.permitAll());
|
||||||
|
|
||||||
|
super.configure(http);
|
||||||
|
|
||||||
|
// This is important to register your login view to the
|
||||||
|
// navigation access control mechanism:
|
||||||
|
setLoginView(http, LoginView.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure(WebSecurity web) throws Exception {
|
||||||
|
// Customize your WebSecurity configuration.
|
||||||
|
super.configure(web);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Demo UserDetailsManager which only provides two hardcoded
|
||||||
|
* in memory users and their roles.
|
||||||
|
* NOTE: This shouldn't be used in real world applications.
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public UserDetailsManager userDetailsService() {
|
||||||
|
UserDetails user =
|
||||||
|
User.withUsername("user")
|
||||||
|
.password("{noop}user")
|
||||||
|
.roles("USER")
|
||||||
|
.build();
|
||||||
|
UserDetails admin =
|
||||||
|
User.withUsername("admin")
|
||||||
|
.password("{noop}admin")
|
||||||
|
.roles("ADMIN")
|
||||||
|
.build();
|
||||||
|
return new InMemoryUserDetailsManager(user, admin);
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,8 +5,6 @@ import jakarta.validation.constraints.Size;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
|
@ -23,20 +21,4 @@ public class Category {
|
||||||
|
|
||||||
@Column(name = "Description")
|
@Column(name = "Description")
|
||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
@Column(name = "userId", nullable = false)
|
|
||||||
private Long userId;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object o) {
|
|
||||||
if (this == o) return true;
|
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
|
||||||
Category category = (Category) o;
|
|
||||||
return id != null && id.equals(category.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return Objects.hash(id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
34
src/main/java/com/application/munera/data/Event.java
Normal file
34
src/main/java/com/application/munera/data/Event.java
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
package com.application.munera.data;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Table(name = "events")
|
||||||
|
public class Event {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Size(max = 100)
|
||||||
|
@Column(name = "Name", nullable = false)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Size(max = 100)
|
||||||
|
@Column(name = "Description")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@ManyToMany(fetch = FetchType.EAGER)
|
||||||
|
@JoinTable(
|
||||||
|
name = "Event_participants",
|
||||||
|
joinColumns = @JoinColumn(name = "event_id"),
|
||||||
|
inverseJoinColumns = @JoinColumn(name = "participant_id"))
|
||||||
|
private Set<Person> participants;
|
||||||
|
}
|
|
@ -1,22 +1,19 @@
|
||||||
package com.application.munera.data;
|
package com.application.munera.data;
|
||||||
|
|
||||||
import com.application.munera.data.enums.ExpenseType;
|
|
||||||
import com.application.munera.data.enums.PeriodUnit;
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import jakarta.validation.constraints.Size;
|
import jakarta.validation.constraints.Size;
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@EqualsAndHashCode
|
|
||||||
@Table(name = "expenses")
|
@Table(name = "expenses")
|
||||||
public class Expense {
|
public class Expense extends AbstractEntity {
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
@ -37,37 +34,39 @@ public class Expense {
|
||||||
@Column(name = "Description")
|
@Column(name = "Description")
|
||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
@Column(name = "Periodic", nullable = false)
|
@Column(name = "PeriodicExpense", nullable = false)
|
||||||
private Boolean isPeriodic;
|
private Boolean isPeriodic;
|
||||||
|
|
||||||
@Enumerated(EnumType.ORDINAL)
|
@Column(name = "PeriodUnit")
|
||||||
@Column(name = "Period")
|
|
||||||
private PeriodUnit periodUnit;
|
private PeriodUnit periodUnit;
|
||||||
|
|
||||||
@Column(name = "PeriodInterval")
|
@Column(name = "PeriodInterval")
|
||||||
private Integer periodInterval;
|
private Integer periodInterval;
|
||||||
|
|
||||||
@ManyToOne
|
@ManyToMany(fetch = FetchType.EAGER)
|
||||||
@JoinColumn(name = "CreditorId")
|
@JoinTable(
|
||||||
private Person payer;
|
name = "Creditor_expenses",
|
||||||
|
joinColumns = @JoinColumn(name = "expense_id"),
|
||||||
|
inverseJoinColumns = @JoinColumn(name = "people_id"))
|
||||||
|
private Set<Person> creditors;
|
||||||
|
|
||||||
|
@ManyToMany(fetch = FetchType.EAGER)
|
||||||
|
@JoinTable(
|
||||||
|
name = "Debtors_expenses",
|
||||||
|
joinColumns = @JoinColumn(name = "expense_id"),
|
||||||
|
inverseJoinColumns = @JoinColumn(name = "people_id"))
|
||||||
|
private Set<Person> debtors;
|
||||||
|
|
||||||
@ManyToOne
|
@ManyToOne
|
||||||
@JoinColumn(name = "DebtorId")
|
@JoinColumn(name = "EventId")
|
||||||
private Person beneficiary;
|
private Event event;
|
||||||
|
|
||||||
@Column(name = "Date", nullable = false, columnDefinition = "DATE DEFAULT CURRENT_DATE")
|
@Column(name = "Date", nullable = false, columnDefinition = "DATE DEFAULT CURRENT_DATE")
|
||||||
private LocalDate date;
|
private LocalDate date;
|
||||||
|
|
||||||
@Column(name = "PaymentDate")
|
/**
|
||||||
private LocalDate paymentDate;
|
* the isResolved field starts as always false, cause at creation an expense cant be already be resolved
|
||||||
|
*/
|
||||||
@Column(name = "isPaid", nullable = false)
|
@Column(name = "isResolved", nullable = false)
|
||||||
private Boolean isPaid = false;
|
private Boolean isResolved = false;
|
||||||
|
}
|
||||||
@Enumerated(EnumType.STRING)
|
|
||||||
@Column(name = "type", nullable = false)
|
|
||||||
private ExpenseType expenseType;
|
|
||||||
|
|
||||||
@Column(name = "userId", nullable = false)
|
|
||||||
private Long userId;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package com.application.munera.data.enums;
|
package com.application.munera.data;
|
||||||
|
|
||||||
|
|
||||||
public enum PeriodUnit {
|
public enum PeriodUnit {
|
|
@ -14,7 +14,7 @@ import java.util.Set;
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@Table(name = "people")
|
@Table(name = "people")
|
||||||
public class Person {
|
public class Person extends AbstractEntity {
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
@ -40,26 +40,24 @@ public class Person {
|
||||||
@Column(name = "credit")
|
@Column(name = "credit")
|
||||||
private BigDecimal credit;
|
private BigDecimal credit;
|
||||||
|
|
||||||
@OneToMany(mappedBy = "payer")
|
@ManyToMany(mappedBy = "creditors")
|
||||||
private Set<Expense> expensesAsPayer;
|
private Set<Expense> creditorExpenses;
|
||||||
|
|
||||||
@OneToMany(mappedBy = "beneficiary")
|
@ManyToMany(mappedBy = "debtors")
|
||||||
private Set<Expense> expensesAsBeneficiary;
|
private Set<Expense> debtorExpenses;
|
||||||
|
|
||||||
@Column(name = "Username", unique = true)
|
@ManyToMany(mappedBy = "participants")
|
||||||
private String username; // This field will link to the User entity
|
private Set<Event> events;
|
||||||
|
|
||||||
@Column(name = "UserId", nullable = false)
|
|
||||||
private Long userId; // Reference to the User entity
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object obj) {
|
public boolean equals(Object obj) {
|
||||||
if (obj == this) {
|
if (obj == this) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (!(obj instanceof Person other)) {
|
if (!(obj instanceof Person)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
Person other = (Person) obj;
|
||||||
return Objects.equals(firstName, other.firstName) &&
|
return Objects.equals(firstName, other.firstName) &&
|
||||||
Objects.equals(lastName, other.lastName) &&
|
Objects.equals(lastName, other.lastName) &&
|
||||||
Objects.equals(email, other.email);
|
Objects.equals(email, other.email);
|
||||||
|
|
|
@ -1,61 +0,0 @@
|
||||||
package com.application.munera.data;
|
|
||||||
|
|
||||||
import com.application.munera.data.enums.Role;
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
import jakarta.validation.constraints.Email;
|
|
||||||
import jakarta.validation.constraints.Size;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Data
|
|
||||||
@Table(name = "users")
|
|
||||||
public class User {
|
|
||||||
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
@Column(name = "Id", unique = true, nullable = false)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@Size(max = 100)
|
|
||||||
@Column(name = "FirstName", nullable = false)
|
|
||||||
private String firstName;
|
|
||||||
|
|
||||||
@Size(max = 100)
|
|
||||||
@Column(name = "LastName", nullable = false)
|
|
||||||
private String lastName;
|
|
||||||
|
|
||||||
@Size(max = 100)
|
|
||||||
@Column(name = "username",unique = true, nullable = false)
|
|
||||||
private String username;
|
|
||||||
|
|
||||||
@Size(max = 100)
|
|
||||||
@Column(name = "password", nullable = false)
|
|
||||||
private String password;
|
|
||||||
|
|
||||||
@Column(name = "roles", nullable = false)
|
|
||||||
private String roles;
|
|
||||||
|
|
||||||
@Email
|
|
||||||
@Size(max = 100)
|
|
||||||
@Column(name = "email")
|
|
||||||
private String email;
|
|
||||||
|
|
||||||
// Helper methods to handle roles as a list of enum values
|
|
||||||
public List<Role> getRoleList() {
|
|
||||||
if (roles == null || roles.isEmpty()) return new ArrayList<>(); // Return an empty list if roles are null or empty
|
|
||||||
return Arrays.stream(roles.split(","))
|
|
||||||
.map(Role::valueOf)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRoleList(List<Role> roleList) {
|
|
||||||
this.roles = roleList.stream()
|
|
||||||
.map(Role::name)
|
|
||||||
.collect(Collectors.joining(","));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
package com.application.munera.data.enums;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
public enum BadgeMessage {
|
|
||||||
PAID_TO_ME("Paid to me", "badge success"),
|
|
||||||
PAID_BY_ME("Paid by me", "badge success"),
|
|
||||||
PAID("Paid", "badge success"),
|
|
||||||
OWED_TO_ME("Owed to me", "badge warning"),
|
|
||||||
OWED_BY_ME("Owed by me", "badge warning"),
|
|
||||||
NOT_PAID("Not paid", "badge warning"),
|
|
||||||
UNKNOWN("Unknown status", "badge error");
|
|
||||||
|
|
||||||
private final String text;
|
|
||||||
private final String theme;
|
|
||||||
|
|
||||||
BadgeMessage(String text, String theme) {
|
|
||||||
this.text = text;
|
|
||||||
this.theme = theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
package com.application.munera.data.enums;
|
|
||||||
|
|
||||||
public enum ExpenseType {
|
|
||||||
CREDIT,
|
|
||||||
DEBIT,
|
|
||||||
NONE
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
package com.application.munera.data.enums;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
public enum Role {
|
|
||||||
ROLE_ADMIN("ROLE_ADMIN"),
|
|
||||||
ROLE_USER("ROLE_USER");
|
|
||||||
|
|
||||||
private final String roleName;
|
|
||||||
|
|
||||||
Role(String roleName) {
|
|
||||||
this.roleName = roleName;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return roleName;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
package com.application.munera.facades;
|
|
||||||
|
|
||||||
import com.application.munera.data.Expense;
|
|
||||||
import com.application.munera.data.Person;
|
|
||||||
import com.application.munera.services.ExpenseService;
|
|
||||||
import com.vaadin.flow.component.notification.Notification;
|
|
||||||
import com.vaadin.flow.component.treegrid.TreeGrid;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class ExpenseFacade {
|
|
||||||
public final ExpenseService expenseService;
|
|
||||||
|
|
||||||
public ExpenseFacade(ExpenseService expenseService) {
|
|
||||||
this.expenseService = expenseService;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets an expense as paid
|
|
||||||
* @param expense the expense to set as paid
|
|
||||||
* @param grid the grid reference to update
|
|
||||||
* @param userId the id of the user related to the expense
|
|
||||||
*/
|
|
||||||
public void setExpensePaid(Expense expense, TreeGrid<Object> grid, Long userId) {
|
|
||||||
expense.setIsPaid(true);
|
|
||||||
this.expenseService.update(expense, userId);
|
|
||||||
Notification.show("Expense " + expense.getName() + " set as paid" );
|
|
||||||
grid.select(null);
|
|
||||||
grid.getDataProvider().refreshAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds all expenses related to a person, both where the person is a payer and a beneficiary.
|
|
||||||
* @param person the person of the expenses
|
|
||||||
* @return the list of expenses found
|
|
||||||
*/
|
|
||||||
public List<Expense> findExpensesByPerson(final Person person) {
|
|
||||||
return this.expenseService.findExpensesByPerson(person);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,151 +0,0 @@
|
||||||
package com.application.munera.facades;
|
|
||||||
|
|
||||||
import com.application.munera.data.Expense;
|
|
||||||
import com.application.munera.data.Person;
|
|
||||||
import com.application.munera.data.User;
|
|
||||||
import com.application.munera.services.ExpenseService;
|
|
||||||
import com.application.munera.services.PersonService;
|
|
||||||
import com.application.munera.services.UserService;
|
|
||||||
import com.vaadin.flow.component.notification.Notification;
|
|
||||||
import com.vaadin.flow.component.notification.NotificationVariant;
|
|
||||||
import com.vaadin.flow.component.treegrid.TreeGrid;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class PersonFacade {
|
|
||||||
|
|
||||||
private final ExpenseService expenseService;
|
|
||||||
private final UserService userService;
|
|
||||||
private final PersonService personService;
|
|
||||||
|
|
||||||
public PersonFacade(ExpenseService expenseService, UserService userService, PersonService personService) {
|
|
||||||
this.expenseService = expenseService;
|
|
||||||
this.userService = userService;
|
|
||||||
this.personService = personService;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches the {@code Person} entity associated with the currently logged-in user.
|
|
||||||
*
|
|
||||||
* @return the {@code Person} entity of the logged-in user, or {@code null} if not found
|
|
||||||
*/
|
|
||||||
public Person getLoggedInPerson() {
|
|
||||||
final var user = userService.getLoggedInUser();
|
|
||||||
return Objects.requireNonNull(personService.findByUsername(user.getUsername()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches all the people related to the user
|
|
||||||
* @param userId the id of the user related to the people
|
|
||||||
* @return the list of people found related to the id of the user
|
|
||||||
*/
|
|
||||||
public List<Person> findAllByUserId(Long userId) {
|
|
||||||
return this.personService.findAllByUserId(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds a {@link Person} entity by its ID.
|
|
||||||
*
|
|
||||||
* @param id the ID of the person to find
|
|
||||||
* @return an {@link Optional} containing the person if found, or an empty {@link Optional} if not found
|
|
||||||
*/
|
|
||||||
public Optional<Person> findById(Long id) {
|
|
||||||
return this.personService.findById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the details of a {@link Person} entity and associates it with a specific user.
|
|
||||||
* This method will save the updated person details and associate it with the given user ID.
|
|
||||||
*
|
|
||||||
* @param person the {@link Person} entity to update
|
|
||||||
* @param userId the ID of the user associated with the person
|
|
||||||
*/
|
|
||||||
public void update(Person person, Long userId) {
|
|
||||||
this.personService.update(person, userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes a {@link Person} entity by its ID.
|
|
||||||
* This method removes the person from the system based on the provided ID.
|
|
||||||
*
|
|
||||||
* @param id the ID of the person to delete
|
|
||||||
*/
|
|
||||||
public void delete(Long id) {
|
|
||||||
this.personService.delete(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds all {@code Person} entities associated with the logged-in user, excluding the logged-in user.
|
|
||||||
*
|
|
||||||
* @param user the logged-in user whose associated persons are to be retrieved
|
|
||||||
* @return a {@code List} of {@code Person} entities excluding the logged-in user
|
|
||||||
*/
|
|
||||||
public List<Person> findAllExcludeLoggedUser(final User user) {
|
|
||||||
return this.personService.findAllExcludeLoggedUser(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates the net balance for a given {@code Person}.
|
|
||||||
*
|
|
||||||
* @param person the {@code Person} for whom the net balance is to be calculated
|
|
||||||
* @return the net balance as a {@code BigDecimal}
|
|
||||||
*/
|
|
||||||
public BigDecimal calculateNetBalance(final Person person) {
|
|
||||||
return this.personService.calculateNetBalance(person);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Marks all expenses as paid for the given {@code Person} where the person is the payer.
|
|
||||||
* Updates the user interface to reflect the changes and provides notifications for success or failure.
|
|
||||||
*
|
|
||||||
* @param person the {@code Person} whose expenses are to be marked as paid
|
|
||||||
* @param grid the {@code TreeGrid} component to refresh after updating expenses
|
|
||||||
* @param userId the ID of the user performing the update
|
|
||||||
*/
|
|
||||||
public void setDebtPaid(Person person, TreeGrid<Object> grid, Long userId) {
|
|
||||||
try {
|
|
||||||
List<Expense> expenses = expenseService.findExpensesWherePayer(person).stream().toList();
|
|
||||||
for (Expense expense : expenses) {
|
|
||||||
expense.setIsPaid(true);
|
|
||||||
expenseService.update(expense, userId);
|
|
||||||
}
|
|
||||||
Notification.show("All expenses marked as paid for " + person.getFirstName() + " " + person.getLastName());
|
|
||||||
grid.select(null);
|
|
||||||
grid.getDataProvider().refreshAll();
|
|
||||||
} catch (Exception e) {
|
|
||||||
Notification n = Notification.show("Error marking expenses as paid: " + e.getMessage());
|
|
||||||
n.setPosition(Notification.Position.MIDDLE);
|
|
||||||
n.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Marks all expenses as paid for the given {@code Person} where the person is the beneficiary.
|
|
||||||
* Updates the user interface to reflect the changes and provides notifications for success or failure.
|
|
||||||
*
|
|
||||||
* @param person the {@code Person} whose expenses are to be marked as paid
|
|
||||||
* @param grid the {@code TreeGrid} component to refresh after updating expenses
|
|
||||||
* @param userId the ID of the user performing the update
|
|
||||||
*/
|
|
||||||
public void setCreditPaid(Person person, TreeGrid<Object> grid, Long userId) {
|
|
||||||
try {
|
|
||||||
List<Expense> expenses = (List<Expense>) expenseService.findExpensesWhereBeneficiary(person);
|
|
||||||
for (Expense expense : expenses) {
|
|
||||||
expense.setIsPaid(true);
|
|
||||||
expenseService.update(expense, userId);
|
|
||||||
}
|
|
||||||
Notification.show("All expenses marked as paid for " + person.getFirstName() + " " + person.getLastName());
|
|
||||||
grid.select(null);
|
|
||||||
grid.getDataProvider().refreshAll();
|
|
||||||
} catch (Exception e) {
|
|
||||||
Notification n = Notification.show("Error marking expenses as paid: " + e.getMessage());
|
|
||||||
n.setPosition(Notification.Position.MIDDLE);
|
|
||||||
n.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,68 +0,0 @@
|
||||||
package com.application.munera.initializers;
|
|
||||||
|
|
||||||
import com.application.munera.data.Category;
|
|
||||||
import com.application.munera.services.CategoryService;
|
|
||||||
import jakarta.annotation.PostConstruct;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class CategoryInitializer {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private CategoryService categoryService;
|
|
||||||
|
|
||||||
@PostConstruct
|
|
||||||
public void init() {
|
|
||||||
if (categoryService.count() == 0) {
|
|
||||||
// Create and save the Food category
|
|
||||||
Category foodCategory = new Category();
|
|
||||||
foodCategory.setName("Food");
|
|
||||||
foodCategory.setUserId(1L);
|
|
||||||
foodCategory.setDescription("All expenses related to food");
|
|
||||||
categoryService.save(foodCategory);
|
|
||||||
|
|
||||||
// Create and save the Travel category
|
|
||||||
Category travelCategory = new Category();
|
|
||||||
travelCategory.setName("Travel");
|
|
||||||
travelCategory.setUserId(1L);
|
|
||||||
travelCategory.setDescription("Expenses related to traveling, including transport and accommodation");
|
|
||||||
categoryService.save(travelCategory);
|
|
||||||
|
|
||||||
// Create and save the Electronics category
|
|
||||||
Category electronicsCategory = new Category();
|
|
||||||
electronicsCategory.setName("Electronics");
|
|
||||||
electronicsCategory.setUserId(1L);
|
|
||||||
electronicsCategory.setDescription("All expenses related to electronic devices and gadgets");
|
|
||||||
categoryService.save(electronicsCategory);
|
|
||||||
|
|
||||||
// Create and save the Events category
|
|
||||||
Category eventsCategory = new Category();
|
|
||||||
eventsCategory.setName("Events");
|
|
||||||
eventsCategory.setUserId(1L);
|
|
||||||
eventsCategory.setDescription("Expenses related to attending or organizing events");
|
|
||||||
categoryService.save(eventsCategory);
|
|
||||||
|
|
||||||
// Create and save the Clothing category
|
|
||||||
Category clothingCategory = new Category();
|
|
||||||
clothingCategory.setName("Clothing");
|
|
||||||
clothingCategory.setUserId(1L);
|
|
||||||
clothingCategory.setDescription("Expenses related to clothes and accessories");
|
|
||||||
categoryService.save(clothingCategory);
|
|
||||||
|
|
||||||
// Create and save the Bills category
|
|
||||||
Category billsCategory = new Category();
|
|
||||||
billsCategory.setName("Bills");
|
|
||||||
billsCategory.setUserId(1L);
|
|
||||||
billsCategory.setDescription("Recurring expenses like utilities, internet, and other bills");
|
|
||||||
categoryService.save(billsCategory);
|
|
||||||
|
|
||||||
// Create and save the Rent category
|
|
||||||
Category rentCategory = new Category();
|
|
||||||
rentCategory.setName("Rent");
|
|
||||||
rentCategory.setUserId(1L);
|
|
||||||
rentCategory.setDescription("Expenses related to rental payments for housing or office space");
|
|
||||||
categoryService.save(rentCategory);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
package com.application.munera.initializers;
|
|
||||||
|
|
||||||
import com.application.munera.data.User;
|
|
||||||
import com.application.munera.security.AdminProperties;
|
|
||||||
import com.application.munera.services.UserService;
|
|
||||||
import jakarta.annotation.PostConstruct;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class UserInitializer {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private UserService userService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private AdminProperties adminProperties;
|
|
||||||
|
|
||||||
@PostConstruct
|
|
||||||
public void init() {
|
|
||||||
if (userService.count() == 0) {
|
|
||||||
User adminUser = new User();
|
|
||||||
adminUser.setUsername(adminProperties.getUsername());
|
|
||||||
adminUser.setPassword(adminProperties.getPassword());
|
|
||||||
adminUser.setFirstName(adminProperties.getFirstName());
|
|
||||||
adminUser.setLastName(adminProperties.getLastName());
|
|
||||||
adminUser.setRoles(adminProperties.getRoles());
|
|
||||||
adminUser.setEmail(adminProperties.getEmail());
|
|
||||||
userService.saveOrUpdateUserAndConnectedPerson(adminUser);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,9 +4,5 @@ import com.application.munera.data.Category;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public interface CategoryRepository extends JpaRepository<Category, Long>, JpaSpecificationExecutor<Category> {
|
public interface CategoryRepository extends JpaRepository<Category, Long>, JpaSpecificationExecutor<Category> {
|
||||||
|
|
||||||
List<Category> findByUserId(Long userId);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
package com.application.munera.repositories;
|
||||||
|
|
||||||
|
import com.application.munera.data.Event;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||||
|
|
||||||
|
public interface EventRepository extends JpaRepository<Event, Long>, JpaSpecificationExecutor<Event> {
|
||||||
|
}
|
|
@ -2,7 +2,6 @@ package com.application.munera.repositories;
|
||||||
|
|
||||||
|
|
||||||
import com.application.munera.data.Expense;
|
import com.application.munera.data.Expense;
|
||||||
import com.application.munera.data.enums.ExpenseType;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
@ -12,57 +11,22 @@ import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
public interface ExpenseRepository extends JpaRepository<Expense, Long>, JpaSpecificationExecutor<Expense> {
|
public interface ExpenseRepository extends JpaRepository<Expense, Long>, JpaSpecificationExecutor<Expense> {
|
||||||
@Query(value = "SELECT DISTINCT e FROM Expense e " +
|
|
||||||
"WHERE (e.payer.id = :userId AND e.beneficiary.id = :userId AND YEAR(e.date) = :year) " +
|
|
||||||
"OR (e.beneficiary.id = :userId AND YEAR(e.date) = :year) " +
|
|
||||||
"OR (e.payer.id = :userId AND e.isPaid = false AND YEAR(e.date) = :year)")
|
|
||||||
List<Expense> findExpensesForDashboard(@Param("userId") Long userId, @Param("year") int year);
|
|
||||||
|
|
||||||
@Query("SELECT DISTINCT YEAR(e.date) FROM Expense e WHERE e.userId = :userId ORDER BY YEAR(e.date)")
|
@Query("SELECT e FROM Expense e JOIN e.creditors c WHERE c.id = :personId")
|
||||||
List<Integer> findExpenseYearsByUserId(@Param("userId") Long userId);
|
Set<Expense> findCreditorsExpensesByPersonId(@Param("personId") Long personId);
|
||||||
|
|
||||||
// Find expenses where the payer is a specific person
|
@Query("SELECT e FROM Expense e JOIN e.debtors d WHERE d.id = :personId")
|
||||||
@Query("SELECT e FROM Expense e WHERE e.payer.id = :personId")
|
Set<Expense> findDebtorsExpensesByPersonId(@Param("personId") Long personId);
|
||||||
List<Expense> findExpensesByPayer(@Param("personId") Long personId);
|
|
||||||
|
|
||||||
// Find expenses where the beneficiary is a specific person
|
|
||||||
@Query("SELECT e FROM Expense e WHERE e.beneficiary.id = :personId")
|
|
||||||
List<Expense> findExpensesByBeneficiary(@Param("personId") Long personId);
|
|
||||||
|
|
||||||
// Find expenses where both payer and beneficiary are the same person
|
|
||||||
@Query("SELECT e FROM Expense e WHERE e.payer.id = :personId AND e.beneficiary.id = :personId")
|
|
||||||
List<Expense> findExpensesByPayerAndBeneficiary(@Param("personId") Long personId);
|
|
||||||
|
|
||||||
// Find expenses where the payer and beneficiary are the same person for a specific year
|
|
||||||
@Query("SELECT e FROM Expense e WHERE e.payer.id = :personId AND e.beneficiary.id = :personId AND YEAR(e.date) = :year")
|
|
||||||
List<Expense> findExpensesByPayerAndBeneficiaryAndYear(@Param("personId") Long personId, @Param("year") int year);
|
|
||||||
|
|
||||||
// Find expenses where the payer is a specific person for a specific year
|
|
||||||
@Query("SELECT e FROM Expense e WHERE e.payer.id = :personId AND YEAR(e.date) = :year")
|
|
||||||
List<Expense> findExpensesByPayerAndYear(@Param("personId") Long personId, @Param("year") int year);
|
|
||||||
|
|
||||||
// Find expenses where the beneficiary is a specific person for a specific year
|
|
||||||
@Query("SELECT e FROM Expense e WHERE e.beneficiary.id = :personId AND YEAR(e.date) = :year")
|
|
||||||
List<Expense> findExpensesByBeneficiaryAndYear(@Param("personId") Long personId, @Param("year") int year);
|
|
||||||
// Find all expenses for a given year
|
|
||||||
@Query("SELECT e FROM Expense e WHERE YEAR(e.date) = :year")
|
@Query("SELECT e FROM Expense e WHERE YEAR(e.date) = :year")
|
||||||
List<Expense> findAllByYear(@Param("year") int year);
|
List<Expense> findAllByYear(@Param("year") int year);
|
||||||
|
|
||||||
// Find unpaid expenses where the creditor is a specific person
|
@Query("SELECT e FROM Expense e JOIN e.creditors c WHERE c.id = :personId AND e.isResolved = false")
|
||||||
@Query("SELECT e FROM Expense e WHERE e.payer.id = :personId AND e.isPaid = false")
|
Set<Expense> findUnpaidCreditorsExpensesByPersonId(@Param("personId") Long personId);
|
||||||
Set<Expense> findUnpaidExpensesByPayer(@Param("personId") Long personId);
|
|
||||||
|
|
||||||
// Find unpaid expenses where the debtor is a specific person
|
@Query("SELECT e FROM Expense e JOIN e.debtors d WHERE d.id = :personId AND e.isResolved = false")
|
||||||
@Query("SELECT e FROM Expense e WHERE e.beneficiary.id = :personId AND e.isPaid = false")
|
Set<Expense> findUnpaidDebtorsExpensesByPersonId(@Param("personId") Long personId);
|
||||||
Set<Expense> findUnpaidExpensesByBeneficiary(@Param("personId") Long personId);
|
|
||||||
|
|
||||||
// Find expenses for a given year and filter by expense type and paid status
|
boolean existsByIdAndIsResolvedTrue(Long id);
|
||||||
@Query("SELECT e FROM Expense e WHERE YEAR(e.date) = :year AND NOT (e.expenseType = :expenseType AND e.isPaid = true)")
|
|
||||||
List<Expense> findByYearAndFilterCreditPaid(@Param("year") int year, @Param("expenseType") ExpenseType expenseType);
|
|
||||||
|
|
||||||
// Check if an expense with the given ID exists and is paid
|
List<Expense> findAllByOrderByDateDesc();}
|
||||||
boolean existsByIdAndIsPaidTrue(Long id);
|
|
||||||
|
|
||||||
// Find all expenses ordered by date descending
|
|
||||||
List<Expense> findByUserIdOrderByDateDesc(Long userId);
|
|
||||||
}
|
|
|
@ -1,30 +1,8 @@
|
||||||
package com.application.munera.repositories;
|
package com.application.munera.repositories;
|
||||||
|
|
||||||
import com.application.munera.data.Person;
|
import com.application.munera.data.Person;
|
||||||
import com.application.munera.data.User;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||||
import org.springframework.data.jpa.repository.Query;
|
|
||||||
import org.springframework.data.repository.query.Param;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public interface PersonRepository extends JpaRepository<Person, Long>, JpaSpecificationExecutor<Person> {
|
public interface PersonRepository extends JpaRepository<Person, Long>, JpaSpecificationExecutor<Person> {
|
||||||
|
|
||||||
Person findByUsername(String username);
|
|
||||||
|
|
||||||
@Query("SELECT p FROM Person p WHERE p.username = :username")
|
|
||||||
Optional<Person> findOptionalByUsername(@Param("username") String username);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* finds all the people that the logged user has created, minus the person that represents the logged user
|
|
||||||
* @param userId the logged user id, to get all people connected to id
|
|
||||||
* @param username the logged username, to filter out
|
|
||||||
* @return the list people found
|
|
||||||
*/
|
|
||||||
@Query("SELECT p FROM Person p WHERE p.userId = :userId AND (p.username IS NULL OR p.username <> :username)")
|
|
||||||
List<Person> findAllByUserIdExcludingPerson(@Param("userId") Long userId, @Param("username") String username);
|
|
||||||
|
|
||||||
List<Person> findByUserId(Long userId);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
package com.application.munera.repositories;
|
|
||||||
|
|
||||||
import com.application.munera.data.User;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import org.springframework.data.jpa.repository.Query;
|
|
||||||
import org.springframework.data.repository.query.Param;
|
|
||||||
import org.springframework.stereotype.Repository;
|
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@Repository
|
|
||||||
public interface UserRepository extends JpaRepository<User, Long> {
|
|
||||||
|
|
||||||
@Query("SELECT u FROM User u WHERE u.username = :username")
|
|
||||||
Optional<User> findByUsername(@Param("username") String username);}
|
|
|
@ -1,19 +0,0 @@
|
||||||
package com.application.munera.security;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@Configuration
|
|
||||||
@ConfigurationProperties(prefix = "admin")
|
|
||||||
public class AdminProperties {
|
|
||||||
private String username;
|
|
||||||
private String password;
|
|
||||||
private String firstName;
|
|
||||||
private String lastName;
|
|
||||||
private String roles;
|
|
||||||
private String email;
|
|
||||||
}
|
|
|
@ -1,64 +0,0 @@
|
||||||
package com.application.munera.security;
|
|
||||||
|
|
||||||
import com.application.munera.repositories.UserRepository;
|
|
||||||
import com.application.munera.views.login.LoginView;
|
|
||||||
import com.vaadin.flow.spring.security.VaadinWebSecurity;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
|
||||||
import org.springframework.security.config.annotation.web.builders.WebSecurity;
|
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
|
||||||
import org.springframework.security.core.userdetails.User;
|
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
|
||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
|
||||||
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
|
|
||||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
|
||||||
import org.springframework.security.provisioning.UserDetailsManager;
|
|
||||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
|
||||||
|
|
||||||
@EnableWebSecurity
|
|
||||||
@Configuration
|
|
||||||
public class SecurityConfiguration extends VaadinWebSecurity {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private UserRepository userRepository;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void configure(HttpSecurity http) throws Exception {
|
|
||||||
http.authorizeHttpRequests(auth -> auth.requestMatchers(new AntPathRequestMatcher("/public/**"))
|
|
||||||
.permitAll());
|
|
||||||
super.configure(http);
|
|
||||||
setLoginView(http, LoginView.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void configure(WebSecurity web) throws Exception {
|
|
||||||
super.configure(web);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public UserDetailsManager userDetailsManager() {
|
|
||||||
return new InMemoryUserDetailsManager() {
|
|
||||||
@Override
|
|
||||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
|
||||||
final var user = userRepository.findByUsername(username)
|
|
||||||
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
|
||||||
|
|
||||||
// Assume roles are stored in the database with prefix, ex: "ROLE_ADMIN"
|
|
||||||
String[] roles = user.getRoles().split(",");
|
|
||||||
|
|
||||||
// Use authorities instead of roles to prevent automatic prefixing
|
|
||||||
return User.withUsername(user.getUsername())
|
|
||||||
.password(user.getPassword())
|
|
||||||
.authorities(roles) // Set roles directly as authorities
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public static NoOpPasswordEncoder passwordEncoder() {
|
|
||||||
return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
package com.application.munera.security;
|
|
||||||
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
|
||||||
|
|
||||||
public class SecurityUtils {
|
|
||||||
|
|
||||||
public static UserDetails getLoggedInUserDetails() {
|
|
||||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
|
||||||
if (authentication != null && authentication.getPrincipal() instanceof UserDetails) {
|
|
||||||
return (UserDetails) authentication.getPrincipal();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
package com.application.munera.services;
|
|
||||||
|
|
||||||
import com.application.munera.data.Expense;
|
|
||||||
import com.vaadin.flow.server.StreamResource;
|
|
||||||
import org.apache.commons.csv.CSVFormat;
|
|
||||||
import org.apache.commons.csv.CSVPrinter;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.OutputStreamWriter;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class CSVService {
|
|
||||||
|
|
||||||
public StreamResource createCSVResource(List<Expense> expenses) {
|
|
||||||
return new StreamResource("expenses.csv", () -> {
|
|
||||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
|
||||||
try (OutputStreamWriter writer = new OutputStreamWriter(stream, StandardCharsets.UTF_8);
|
|
||||||
CSVPrinter csvPrinter = new CSVPrinter(writer, CSVFormat.DEFAULT.withHeader("Name", "Cost", "Category", "Date", "Payment date"))) {
|
|
||||||
|
|
||||||
for (Expense expense : expenses) {
|
|
||||||
csvPrinter.printRecord(
|
|
||||||
expense.getName(),
|
|
||||||
expense.getCost(),
|
|
||||||
expense.getCategory() != null ? expense.getCategory().getName() : "",
|
|
||||||
expense.getDate(),
|
|
||||||
expense.getPaymentDate() != null ? expense.getPaymentDate() : "Unpaid"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
return new ByteArrayInputStream(stream.toByteArray());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -18,72 +18,23 @@ public class CategoryService {
|
||||||
this.categoryRepository = categoryRepository;
|
this.categoryRepository = categoryRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds a category by its ID.
|
|
||||||
*
|
|
||||||
* @param id the ID of the category to find
|
|
||||||
* @return an {@code Optional} containing the found category, or {@code Optional.empty()} if no category with the given ID exists
|
|
||||||
*/
|
|
||||||
public Optional<Category> findById(Long id) {
|
public Optional<Category> findById(Long id) {
|
||||||
return categoryRepository.findById(id);
|
return categoryRepository.findById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public List<Category> findAll() {
|
||||||
* Finds all categories associated with a specific user ID.
|
return categoryRepository.findAll();
|
||||||
*
|
|
||||||
* @param userId the ID of the user whose categories are to be retrieved
|
|
||||||
* @return a {@code List} of categories associated with the given user ID
|
|
||||||
*/
|
|
||||||
public List<Category> findAllByUserId(Long userId) {
|
|
||||||
return categoryRepository.findByUserId(userId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public void update(Category category) {
|
||||||
* Updates the provided category and associates it with a specific user ID.
|
|
||||||
*
|
|
||||||
* @param category the category to update
|
|
||||||
* @param userId the ID of the user to associate with the category
|
|
||||||
*/
|
|
||||||
public void update(Category category, Long userId) {
|
|
||||||
category.setUserId(userId);
|
|
||||||
categoryRepository.save(category);
|
categoryRepository.save(category);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes the given category.
|
|
||||||
*
|
|
||||||
* @param category the category to delete
|
|
||||||
*/
|
|
||||||
public void delete(Category category) {
|
public void delete(Category category) {
|
||||||
categoryRepository.delete(category);
|
categoryRepository.delete(category);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves a paginated list of categories.
|
|
||||||
*
|
|
||||||
* @param pageable the pagination information
|
|
||||||
* @return a {@code Page} containing the categories for the requested page
|
|
||||||
*/
|
|
||||||
public Page<Category> list(Pageable pageable){
|
public Page<Category> list(Pageable pageable){
|
||||||
return categoryRepository.findAll(pageable);
|
return categoryRepository.findAll(pageable);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Counts the total number of categories.
|
|
||||||
*
|
|
||||||
* @return the total number of categories
|
|
||||||
*/
|
|
||||||
public Long count() {
|
|
||||||
return this.categoryRepository.count();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves the given category to the repository.
|
|
||||||
*
|
|
||||||
* @param category the category to save
|
|
||||||
* @return the saved category
|
|
||||||
*/
|
|
||||||
public Category save(Category category) {
|
|
||||||
return this.categoryRepository.save(category);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
package com.application.munera.services;
|
||||||
|
|
||||||
|
import com.application.munera.data.Event;
|
||||||
|
import com.application.munera.repositories.EventRepository;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class EventService {
|
||||||
|
|
||||||
|
private final EventRepository eventRepository;
|
||||||
|
|
||||||
|
public EventService(final EventRepository eventRepository){
|
||||||
|
this.eventRepository = eventRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Event> findById(Long id) {
|
||||||
|
return eventRepository.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Event> findAll() {
|
||||||
|
return eventRepository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update(Event event) {
|
||||||
|
eventRepository.save(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(Event event) {
|
||||||
|
eventRepository.delete(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Page<Event> list(Pageable pageable){
|
||||||
|
return eventRepository.findAll(pageable);
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,222 +2,84 @@ package com.application.munera.services;
|
||||||
|
|
||||||
import com.application.munera.data.Expense;
|
import com.application.munera.data.Expense;
|
||||||
import com.application.munera.data.Person;
|
import com.application.munera.data.Person;
|
||||||
import com.application.munera.data.enums.ExpenseType;
|
|
||||||
import com.application.munera.repositories.ExpenseRepository;
|
import com.application.munera.repositories.ExpenseRepository;
|
||||||
import com.application.munera.repositories.PersonRepository;
|
|
||||||
import com.application.munera.repositories.UserRepository;
|
|
||||||
import com.application.munera.security.SecurityUtils;
|
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.jpa.domain.Specification;
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import java.util.Collection;
|
||||||
import java.time.LocalDate;
|
import java.util.List;
|
||||||
import java.time.Year;
|
import java.util.Optional;
|
||||||
import java.util.*;
|
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class ExpenseService {
|
public class ExpenseService {
|
||||||
|
|
||||||
private final ExpenseRepository expenseRepository;
|
private final ExpenseRepository repository;
|
||||||
private final UserRepository userRepository;
|
|
||||||
private final PersonRepository personRepository;
|
|
||||||
|
|
||||||
public ExpenseService(ExpenseRepository expenseRepository, UserRepository userRepository, PersonRepository personRepository) {
|
public ExpenseService(ExpenseRepository repository) {
|
||||||
this.expenseRepository = expenseRepository;
|
this.repository = repository;
|
||||||
this.userRepository = userRepository;
|
|
||||||
this.personRepository = personRepository;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves an expense by its ID.
|
|
||||||
* @param id the ID of the expense
|
|
||||||
* @return an Optional containing the expense if found, otherwise empty
|
|
||||||
*/
|
|
||||||
public Optional<Expense> get(Long id) {
|
public Optional<Expense> get(Long id) {
|
||||||
return expenseRepository.findById(id);
|
return repository.findById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public Collection<Expense> findDebtByUser(final Person person) {
|
||||||
* Finds all expenses where the specified person is the beneficiary.
|
return repository.findDebtorsExpensesByPersonId(person.getId());
|
||||||
* @param person the user of the expenses
|
|
||||||
* @return the collection of expenses found
|
|
||||||
*/
|
|
||||||
public Collection<Expense> findExpensesWhereBeneficiary(final Person person) {
|
|
||||||
return expenseRepository.findExpensesByBeneficiary(person.getId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public Collection<Expense> findCreditByUser(final Person person) {
|
||||||
* Finds all expenses where the specified person is the payer.
|
return repository.findCreditorsExpensesByPersonId(person.getId());
|
||||||
* @param person the user of the expenses
|
|
||||||
* @return the collection of expenses found
|
|
||||||
*/
|
|
||||||
public Collection<Expense> findExpensesWherePayer(final Person person) {
|
|
||||||
return expenseRepository.findExpensesByPayer(person.getId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public Collection<Expense> findUnpaidDebtByUser(final Person person) {
|
||||||
* Finds all expenses where the specified person is the beneficiary and the expense is unpaid.
|
return repository.findUnpaidDebtorsExpensesByPersonId(person.getId());
|
||||||
* @param person the user of the expenses
|
|
||||||
* @return the collection of unpaid expenses found
|
|
||||||
*/
|
|
||||||
public Collection<Expense> findUnpaidExpensesWhereBeneficiary(final Person person) {
|
|
||||||
return expenseRepository.findUnpaidExpensesByBeneficiary(person.getId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public Collection<Expense> findUnpaidCreditByUser(final Person person) {
|
||||||
* Finds all expenses where the specified person is the payer and the expense is unpaid.
|
return repository.findUnpaidCreditorsExpensesByPersonId(person.getId());
|
||||||
* @param person the user of the expenses
|
|
||||||
* @return the collection of unpaid expenses found
|
|
||||||
*/
|
|
||||||
public Collection<Expense> findUnpaidExpensesWherePayer(final Person person) {
|
|
||||||
return expenseRepository.findUnpaidExpensesByPayer(person.getId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public List<Expense> findExpenseByUser(final Person person) {
|
||||||
* Finds all expenses related to a person, both where the person is a payer and a beneficiary.
|
final var credits = this.findCreditByUser(person);
|
||||||
* @param person the person of the expenses
|
final var debits = this.findDebtByUser(person);
|
||||||
* @return the list of expenses found
|
return Stream.concat(credits.stream(), debits.stream()).toList();
|
||||||
*/
|
|
||||||
public List<Expense> findExpensesByPerson(final Person person) {
|
|
||||||
// Retrieve expenses where the person is the payer
|
|
||||||
final var payerExpenses = this.findExpensesWherePayer(person);
|
|
||||||
// Retrieve expenses where the person is the beneficiary
|
|
||||||
final var beneficiaryExpenses = this.findExpensesWhereBeneficiary(person);
|
|
||||||
// Combine both sets of expenses into a single list without duplicates
|
|
||||||
return Stream.concat(payerExpenses.stream(), beneficiaryExpenses.stream())
|
|
||||||
.distinct()
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Retrieves all expenses.
|
|
||||||
* @return the list of all expenses
|
|
||||||
*/
|
|
||||||
public List<Expense> findAll() {
|
|
||||||
return expenseRepository.findAll();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public List<Expense> findAll() {return repository.findAll();}
|
||||||
* Finds all expenses for a given year.
|
|
||||||
* @param year the year for which to find expenses
|
public void update(Expense entity) {
|
||||||
* @return the list of expenses found
|
repository.save(entity);
|
||||||
*/
|
|
||||||
public List<Expense> findAllByYear(final int year) {
|
|
||||||
return this.expenseRepository.findAllByYear(year);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches all expenses ordered by date in descending order.
|
|
||||||
* @return the list of expenses found
|
|
||||||
*/
|
|
||||||
public List<Expense> findAllOrderByDateDescending(Long userId) {
|
|
||||||
return this.expenseRepository.findByUserIdOrderByDateDesc(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if an expense has been paid.
|
|
||||||
* @param expense the expense to check
|
|
||||||
* @return true if the expense has been paid, false otherwise
|
|
||||||
*/
|
|
||||||
public boolean isExpensePaid(final Expense expense) {
|
|
||||||
return this.expenseRepository.existsByIdAndIsPaidTrue(expense.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates an existing expense.
|
|
||||||
* @param entity the expense to update
|
|
||||||
*/
|
|
||||||
public void update(Expense entity, Long userId) {
|
|
||||||
entity.setUserId(userId);
|
|
||||||
if (Boolean.TRUE.equals(entity.getIsPaid())) entity.setPaymentDate(LocalDate.now());
|
|
||||||
else entity.setPaymentDate(null);
|
|
||||||
this.setExpenseType(entity);
|
|
||||||
expenseRepository.save(entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes an expense given its ID.
|
|
||||||
* @param id the ID of the expense to delete
|
|
||||||
*/
|
|
||||||
public void delete(Long id) {
|
public void delete(Long id) {
|
||||||
expenseRepository.deleteById(id);
|
repository.deleteById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Lists expenses in a paginated format.
|
|
||||||
* @param pageable the pagination information
|
|
||||||
* @return a page of expenses
|
|
||||||
*/
|
|
||||||
public Page<Expense> list(Pageable pageable) {
|
public Page<Expense> list(Pageable pageable) {
|
||||||
return expenseRepository.findAll(pageable);
|
return repository.findAll(pageable);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Lists expenses in a paginated format with filtering options.
|
|
||||||
* @param pageable the pagination information
|
|
||||||
* @param filter the filter specification
|
|
||||||
* @return a page of expenses matching the filter
|
|
||||||
*/
|
|
||||||
public Page<Expense> list(Pageable pageable, Specification<Expense> filter) {
|
public Page<Expense> list(Pageable pageable, Specification<Expense> filter) {
|
||||||
return expenseRepository.findAll(filter, pageable);
|
return repository.findAll(filter, pageable);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Counts the total number of expenses.
|
|
||||||
* @return the count of expenses
|
|
||||||
*/
|
|
||||||
public int count() {
|
public int count() {
|
||||||
return (int) expenseRepository.count();
|
return (int) repository.count();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public List<Expense> findAllByYear(final int year ) {
|
||||||
* Fetches the yearly net expenses of the user
|
return this.repository.findAllByYear(year);
|
||||||
* @param loggedInPerson the logged-in user
|
|
||||||
* @param year the year from which we want the expenses
|
|
||||||
* @return the list of expenses of that user in that year
|
|
||||||
*/
|
|
||||||
public List<Expense> fetchExpensesForDashboard(Person loggedInPerson, Year year) {
|
|
||||||
return this.expenseRepository.findExpensesForDashboard(loggedInPerson.getUserId(), year.getValue());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public boolean isExpenseResolved(final Expense expense) {
|
||||||
* Gets the list of years in which at least one expense has been made
|
return this.repository.existsByIdAndIsResolvedTrue(expense.getId());
|
||||||
* @param userId the user from which to look into the expenses
|
|
||||||
* @return the list of years found
|
|
||||||
*/
|
|
||||||
public List<Integer> getAvailableExpenseYearsForUser(Long userId) {
|
|
||||||
return expenseRepository.findExpenseYearsByUserId(userId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================================
|
public List<Expense> findAllOrderByDateDescending() {
|
||||||
// Private methods
|
return this.repository.findAllByOrderByDateDesc();
|
||||||
// ================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the expense type depending on the presence or absence of a payer and beneficiary.
|
|
||||||
* @param expense the expense to set the type of
|
|
||||||
*/
|
|
||||||
private void setExpenseType(final @Nonnull Expense expense) {
|
|
||||||
// Get the currently logged-in user
|
|
||||||
final var userDetails = SecurityUtils.getLoggedInUserDetails();
|
|
||||||
if (userDetails == null) throw new IllegalStateException("No logged-in user found");
|
|
||||||
// Fetch the logged-in user
|
|
||||||
final var loggedInUser = userRepository.findByUsername(userDetails.getUsername())
|
|
||||||
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
|
||||||
Person loggedInPerson = this.personRepository.findByUsername(loggedInUser.getUsername());
|
|
||||||
|
|
||||||
if (loggedInPerson == null) throw new IllegalStateException("No associated Person entity found for logged-in user");
|
|
||||||
|
|
||||||
// Check if the payer and beneficiary are present
|
|
||||||
Person payer = expense.getPayer();
|
|
||||||
Person beneficiary = expense.getBeneficiary();
|
|
||||||
|
|
||||||
// Determine the expense type
|
|
||||||
if (payer.equals(loggedInPerson) && !beneficiary.equals(loggedInPerson)) expense.setExpenseType(ExpenseType.CREDIT); // Logged-in user is the payer, and the beneficiary is someone else
|
|
||||||
else if (!payer.equals(loggedInPerson) && beneficiary.equals(loggedInPerson)) expense.setExpenseType(ExpenseType.DEBIT); // Logged-in user is the beneficiary, and the payer is someone else
|
|
||||||
else if (payer.equals(loggedInPerson)) expense.setExpenseType(ExpenseType.NONE); // Both payer and beneficiary are the logged-in user
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
}
|
||||||
|
|
|
@ -2,8 +2,6 @@ package com.application.munera.services;
|
||||||
|
|
||||||
import com.application.munera.data.Expense;
|
import com.application.munera.data.Expense;
|
||||||
import com.application.munera.data.Person;
|
import com.application.munera.data.Person;
|
||||||
import com.application.munera.data.User;
|
|
||||||
import com.application.munera.repositories.ExpenseRepository;
|
|
||||||
import com.application.munera.repositories.PersonRepository;
|
import com.application.munera.repositories.PersonRepository;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
|
@ -11,138 +9,59 @@ import org.springframework.data.jpa.domain.Specification;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.List;
|
import java.util.Collection;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class PersonService {
|
public class PersonService {
|
||||||
|
|
||||||
private final PersonRepository personRepository;
|
private final PersonRepository personRepository;
|
||||||
private final ExpenseRepository expenseRepository;
|
private final ExpenseService expenseService;
|
||||||
|
|
||||||
public PersonService(PersonRepository personRepository, ExpenseRepository expenseRepository) {
|
public PersonService(PersonRepository personRepository, ExpenseService expenseService) {
|
||||||
this.personRepository = personRepository;
|
this.personRepository = personRepository;
|
||||||
this.expenseRepository = expenseRepository;
|
this.expenseService = expenseService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public Optional<Person> get(Long id) {
|
||||||
* Finds a person by ID.
|
|
||||||
* @param id the ID of the person
|
|
||||||
* @return an optional containing the person if found, otherwise empty
|
|
||||||
*/
|
|
||||||
public Optional<Person> findById(Long id) {
|
|
||||||
return personRepository.findById(id);
|
return personRepository.findById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public Collection<Person> findAll() {
|
||||||
* Finds all people.
|
return this.personRepository.findAll();
|
||||||
* @return a collection of all persons
|
|
||||||
*/
|
|
||||||
public List<Person> findAllByUserId(Long userId) {
|
|
||||||
return this.personRepository.findByUserId(userId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Person findByUsername(String username) {
|
public void update(Person person) {
|
||||||
return this.personRepository.findByUsername(username);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds all people excluding the users'ones.
|
|
||||||
* @return a collection of all persons
|
|
||||||
*/
|
|
||||||
public List<Person> findAllExcludeLoggedUser(User user) {
|
|
||||||
final var userId = user.getId();
|
|
||||||
final var username = user.getUsername();
|
|
||||||
return this.personRepository.findAllByUserIdExcludingPerson(userId, username);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lists all persons with pagination.
|
|
||||||
* @param pageable the pagination information
|
|
||||||
* @return a page of persons
|
|
||||||
*/
|
|
||||||
public Page<Person> list(Pageable pageable) {
|
|
||||||
return personRepository.findAll(pageable);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lists all persons with pagination and filtering.
|
|
||||||
* @param pageable the pagination information
|
|
||||||
* @param filter the specification filter
|
|
||||||
* @return a page of persons matching the filter
|
|
||||||
*/
|
|
||||||
public Page<Person> list(Pageable pageable, Specification<Person> filter) {
|
|
||||||
return this.personRepository.findAll(filter, pageable);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Counts the total number of persons.
|
|
||||||
* @return the total count of persons
|
|
||||||
*/
|
|
||||||
public int count() {
|
|
||||||
return (int) this.personRepository.count();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates a person in the repository.
|
|
||||||
* @param person the person to update
|
|
||||||
*/
|
|
||||||
public void update(Person person, Long userId) {
|
|
||||||
person.setUserId(userId);
|
|
||||||
this.personRepository.save(person);
|
this.personRepository.save(person);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes a person by ID.
|
|
||||||
* @param id the ID of the person to delete
|
|
||||||
*/
|
|
||||||
public void delete(Long id) {
|
public void delete(Long id) {
|
||||||
this.personRepository.deleteById(id);
|
this.personRepository.deleteById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public Page<Person> list(Pageable pageable){
|
||||||
* Calculates the total debt of a person.
|
return personRepository.findAll(pageable);
|
||||||
* @param person the person whose debt is to be calculated
|
}
|
||||||
* @return the total debt amount
|
|
||||||
*/
|
public Page<Person> list(Pageable pageable, Specification<Person> filter) {
|
||||||
public BigDecimal calculateDebt(final Person person) {
|
return this.personRepository.findAll(filter, pageable);
|
||||||
return this.expenseRepository.findExpensesByPayer(person.getId()).stream()
|
}
|
||||||
.filter(expense -> !expense.getBeneficiary().equals(person) && Boolean.FALSE.equals(expense.getIsPaid()))
|
|
||||||
.map(Expense::getCost)
|
public int count() {
|
||||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
return (int) this.personRepository.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal calculateDebt(final Person person){
|
||||||
|
return this.expenseService.findDebtByUser(person).stream().map(Expense::getCost).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates the total credit of a person.
|
|
||||||
* @param person the person whose credit is to be calculated
|
|
||||||
* @return the total credit amount
|
|
||||||
*/
|
|
||||||
public BigDecimal calculateCredit(final Person person) {
|
public BigDecimal calculateCredit(final Person person) {
|
||||||
return this.expenseRepository.findExpensesByBeneficiary(person.getId()).stream()
|
return this.expenseService.findCreditByUser(person).stream().map(Expense::getCost).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
.filter(expense -> !expense.getPayer().equals(person) && Boolean.FALSE.equals(expense.getIsPaid()))
|
|
||||||
.map(Expense::getCost)
|
|
||||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates the net balance of a person.
|
|
||||||
* The net balance is the difference between the total amount the person is owed
|
|
||||||
* (expenses where they are the payer) and the total amount the person owes
|
|
||||||
* (expenses where they are the beneficiary).
|
|
||||||
*
|
|
||||||
* A positive net balance means the person is owed money.
|
|
||||||
* A negative net balance means the person owes money.
|
|
||||||
*
|
|
||||||
* @param person the person whose net balance is to be calculated
|
|
||||||
* @return the net balance amount
|
|
||||||
*/
|
|
||||||
public BigDecimal calculateNetBalance(final Person person) {
|
public BigDecimal calculateNetBalance(final Person person) {
|
||||||
// Calculate total debt (what others owe to the person)
|
final var credit = this.expenseService.findUnpaidCreditByUser(person).stream().map(Expense::getCost).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
final BigDecimal debt = this.calculateDebt(person);
|
final var debit = this.expenseService.findUnpaidDebtByUser(person).stream().map(Expense::getCost).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
// Calculate total credit (what the person owes to others)
|
return credit.subtract(debit);
|
||||||
final BigDecimal credit = this.calculateCredit(person);
|
|
||||||
// Net balance calculation: debt (owed to the person) - credit (person owes)
|
|
||||||
return debt.subtract(credit);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,151 +0,0 @@
|
||||||
package com.application.munera.services;
|
|
||||||
|
|
||||||
import com.application.munera.data.Person;
|
|
||||||
import com.application.munera.data.User;
|
|
||||||
import com.application.munera.repositories.PersonRepository;
|
|
||||||
import com.application.munera.repositories.UserRepository;
|
|
||||||
import jakarta.transaction.Transactional;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
import static com.application.munera.security.SecurityUtils.getLoggedInUserDetails;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class UserService {
|
|
||||||
|
|
||||||
private final UserRepository userRepository;
|
|
||||||
private final PersonRepository personRepository;
|
|
||||||
|
|
||||||
public UserService(UserRepository userRepository, PersonRepository personRepository) {
|
|
||||||
this.userRepository = userRepository;
|
|
||||||
this.personRepository = personRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves all {@link User} entities.
|
|
||||||
*
|
|
||||||
* @return a list of all users
|
|
||||||
*/
|
|
||||||
public List<User> findAll() {
|
|
||||||
return this.userRepository.findAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds a {@link User} entity by its ID.
|
|
||||||
*
|
|
||||||
* @param id the ID of the user to find
|
|
||||||
* @return an {@link Optional} containing the user if found, or an empty {@link Optional} if not found
|
|
||||||
*/
|
|
||||||
public Optional<User> findById(final Long id) {
|
|
||||||
return this.userRepository.findById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds a {@link User} entity by its username.
|
|
||||||
*
|
|
||||||
* @param username the username of the user to find
|
|
||||||
* @return an {@link Optional} containing the user if found, or an empty {@link Optional} if not found
|
|
||||||
*/
|
|
||||||
public Optional<User> findByUsername(String username) {
|
|
||||||
return this.userRepository.findByUsername(username);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches the {@link User} entity of the currently logged-in user.
|
|
||||||
*
|
|
||||||
* @return the logged-in {@link User} entity
|
|
||||||
* @throws IllegalStateException if the user is not logged in or not found
|
|
||||||
*/
|
|
||||||
public User getLoggedInUser() {
|
|
||||||
final var userDetails = getLoggedInUserDetails();
|
|
||||||
if (userDetails == null) throw new IllegalStateException("User is not logged in.");
|
|
||||||
final var username = userDetails.getUsername();
|
|
||||||
return userRepository.findByUsername(username)
|
|
||||||
.orElseThrow(() -> new IllegalStateException("User not found: " + username));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves or updates a {@link User} entity and its associated {@link Person} entity.
|
|
||||||
* If the user already exists, it updates the user and the associated person.
|
|
||||||
* If the user does not exist, it creates a new user and a new person entity.
|
|
||||||
*
|
|
||||||
* @param user the {@link User} entity to save or update
|
|
||||||
*/
|
|
||||||
@Transactional
|
|
||||||
public void saveOrUpdateUserAndConnectedPerson(User user) {
|
|
||||||
// Check if the user already exists in the database
|
|
||||||
final var existingUserOptional = userRepository.findByUsername(user.getUsername());
|
|
||||||
User userToSave = getUser(user, existingUserOptional);
|
|
||||||
|
|
||||||
// Save the user entity
|
|
||||||
userRepository.save(userToSave);
|
|
||||||
|
|
||||||
// Check if the associated person exists for the user
|
|
||||||
final var existingPersonOptional = personRepository.findOptionalByUsername(userToSave.getUsername());
|
|
||||||
|
|
||||||
if (existingPersonOptional.isPresent()) {
|
|
||||||
// If person exists, update the person entity
|
|
||||||
Person personToUpdate = existingPersonOptional.get();
|
|
||||||
personToUpdate.setFirstName(userToSave.getFirstName());
|
|
||||||
personToUpdate.setLastName(userToSave.getLastName());
|
|
||||||
personToUpdate.setUsername(user.getUsername());
|
|
||||||
personToUpdate.setEmail(userToSave.getEmail());
|
|
||||||
personRepository.save(personToUpdate);
|
|
||||||
} else {
|
|
||||||
// If no person is associated with the user, create a new Person entity and link it to the User
|
|
||||||
Person newPerson = new Person();
|
|
||||||
newPerson.setFirstName(userToSave.getFirstName());
|
|
||||||
newPerson.setLastName(userToSave.getLastName());
|
|
||||||
newPerson.setEmail(userToSave.getEmail());
|
|
||||||
newPerson.setUsername(userToSave.getUsername());
|
|
||||||
newPerson.setUserId(userToSave.getId());
|
|
||||||
personRepository.save(newPerson);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Counts the total number of {@link User} entities.
|
|
||||||
*
|
|
||||||
* @return the total number of users
|
|
||||||
*/
|
|
||||||
public Long count() {
|
|
||||||
return this.userRepository.count();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes the specified {@link User} entity and its associated {@link Person} entity.
|
|
||||||
*
|
|
||||||
* @param user the {@link User} entity to delete
|
|
||||||
*/
|
|
||||||
public void delete(final User user) {
|
|
||||||
this.userRepository.delete(user);
|
|
||||||
final var person = this.personRepository.findByUsername(user.getUsername());
|
|
||||||
this.personRepository.delete(person);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines whether a user exists and creates or updates the {@link User} entity accordingly.
|
|
||||||
*
|
|
||||||
* @param user the {@link User} entity to save or update
|
|
||||||
* @param existingUserOptional an {@link Optional} containing an existing user if found
|
|
||||||
* @return the user to be saved
|
|
||||||
*/
|
|
||||||
private User getUser(User user, Optional<User> existingUserOptional) {
|
|
||||||
User userToSave;
|
|
||||||
if (existingUserOptional.isPresent()) {
|
|
||||||
// If user exists, update the user entity
|
|
||||||
userToSave = existingUserOptional.get();
|
|
||||||
userToSave.setFirstName(user.getFirstName());
|
|
||||||
userToSave.setLastName(user.getLastName());
|
|
||||||
userToSave.setEmail(user.getEmail());
|
|
||||||
userToSave.setUsername(user.getUsername());
|
|
||||||
userToSave.setPassword(user.getPassword());
|
|
||||||
userToSave.setRoles(user.getRoles());
|
|
||||||
} else {
|
|
||||||
userToSave = user; // If user does not exist, save the new user entity
|
|
||||||
}
|
|
||||||
return userToSave;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,125 +0,0 @@
|
||||||
package com.application.munera.services;
|
|
||||||
|
|
||||||
import com.application.munera.data.enums.BadgeMessage;
|
|
||||||
import com.application.munera.data.Category;
|
|
||||||
import com.application.munera.data.Expense;
|
|
||||||
import com.application.munera.data.enums.ExpenseType;
|
|
||||||
import com.vaadin.flow.component.combobox.MultiSelectComboBox;
|
|
||||||
import com.vaadin.flow.component.html.Span;
|
|
||||||
import com.vaadin.flow.component.textfield.TextField;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.vaadin.klaudeta.PaginatedGrid;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class ViewsService {
|
|
||||||
|
|
||||||
private final ExpenseService expenseService;
|
|
||||||
|
|
||||||
public ViewsService(ExpenseService expenseService) {
|
|
||||||
this.expenseService = expenseService;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a badge (Vaadin {@link Span}) based on the type of the expense and its payment status.
|
|
||||||
*
|
|
||||||
* @param expense the expense for which the badge is being created.
|
|
||||||
* @return a {@link Span} object representing the expense status badge.
|
|
||||||
*/
|
|
||||||
public Span createExpenseBadge(final Expense expense) {
|
|
||||||
final var isExpensePaid = Boolean.TRUE.equals(this.expenseService.isExpensePaid(expense));
|
|
||||||
final var badgeMessage = determineBadgeMessage(expense.getExpenseType(), isExpensePaid);
|
|
||||||
|
|
||||||
final var badge = new Span();
|
|
||||||
badge.setText(badgeMessage.getText());
|
|
||||||
badge.getElement().getThemeList().add(badgeMessage.getTheme());
|
|
||||||
|
|
||||||
return badge;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a badge (Vaadin {@link Span}) that reflects a person's financial balance status
|
|
||||||
* based on the net balance provided.
|
|
||||||
*
|
|
||||||
* @param netBalance the net balance of the person.
|
|
||||||
* @return a {@link Span} object representing the person's financial status badge.
|
|
||||||
*/
|
|
||||||
public Span createPersonBadge(BigDecimal netBalance) {
|
|
||||||
Span badge = new Span();
|
|
||||||
if (netBalance.compareTo(BigDecimal.ZERO) < 0) {
|
|
||||||
badge.setText("Credit");
|
|
||||||
badge.getElement().getThemeList().add("badge success");
|
|
||||||
} else if (netBalance.compareTo(BigDecimal.ZERO) > 0) {
|
|
||||||
badge.setText("Debit");
|
|
||||||
badge.getElement().getThemeList().add("badge error");
|
|
||||||
} else {
|
|
||||||
badge.setText("Clear");
|
|
||||||
badge.getElement().getThemeList().add("badge contrast");
|
|
||||||
}
|
|
||||||
return badge;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies a name-based filter on the expenses displayed in the provided grid.
|
|
||||||
* If the filter value is empty, all expenses are displayed; otherwise, expenses
|
|
||||||
* whose names match the filter are shown.
|
|
||||||
*
|
|
||||||
* @param nameFilter the {@link TextField} containing the name filter value.
|
|
||||||
* @param userId the ID of the user whose expenses are being filtered.
|
|
||||||
* @param grid the {@link PaginatedGrid} that displays the expenses.
|
|
||||||
*/
|
|
||||||
public void applyNameFilter(TextField nameFilter, Long userId, PaginatedGrid<Expense, Objects> grid) {
|
|
||||||
final var filterValue = nameFilter.getValue().trim();
|
|
||||||
List<Expense> filteredExpenses;
|
|
||||||
if (filterValue.isEmpty()) filteredExpenses = expenseService.findAllOrderByDateDescending(userId); // If the filter is empty, return all expenses
|
|
||||||
else {
|
|
||||||
// Apply the filter (e.g., by name)
|
|
||||||
filteredExpenses = expenseService.findAllOrderByDateDescending(userId)
|
|
||||||
.stream()
|
|
||||||
.filter(expense -> expense.getName().toLowerCase().contains(filterValue.toLowerCase())).toList();
|
|
||||||
}
|
|
||||||
grid.setItems(filteredExpenses);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies a category-based filter on the expenses displayed in the provided grid.
|
|
||||||
* If no categories are selected, all expenses are shown; otherwise, only expenses
|
|
||||||
* that match the selected categories are displayed.
|
|
||||||
*
|
|
||||||
* @param categoryFilter the {@link MultiSelectComboBox} containing the selected categories.
|
|
||||||
* @param userId the ID of the user whose expenses are being filtered.
|
|
||||||
* @param grid the {@link PaginatedGrid} that displays the expenses.
|
|
||||||
*/
|
|
||||||
public void applyCategoryFilter(MultiSelectComboBox<Category> categoryFilter, Long userId, PaginatedGrid<Expense, Objects> grid) {
|
|
||||||
final var selectedCategories = categoryFilter.getValue();
|
|
||||||
List<Expense> filteredExpenses;
|
|
||||||
if (selectedCategories.isEmpty()) filteredExpenses = expenseService.findAllOrderByDateDescending(userId); // If no categories are selected, return all expenses
|
|
||||||
else {
|
|
||||||
// Apply the filter by selected categories
|
|
||||||
filteredExpenses = expenseService.findAllOrderByDateDescending(userId)
|
|
||||||
.stream()
|
|
||||||
.filter(expense1 -> selectedCategories.contains(expense1.getCategory()))
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
grid.setItems(filteredExpenses);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines the message and theme for a badge based on the expense type and its payment status.
|
|
||||||
*
|
|
||||||
* @param type the type of the expense (CREDIT, DEBIT, or NONE).
|
|
||||||
* @param isPaid boolean indicating whether the expense has been paid.
|
|
||||||
* @return a {@link BadgeMessage} object containing the text and theme for the badge.
|
|
||||||
*/
|
|
||||||
private BadgeMessage determineBadgeMessage(ExpenseType type, boolean isPaid) {
|
|
||||||
return switch (type) {
|
|
||||||
case CREDIT -> isPaid ? BadgeMessage.PAID_TO_ME : BadgeMessage.OWED_TO_ME;
|
|
||||||
case DEBIT -> isPaid ? BadgeMessage.PAID_BY_ME : BadgeMessage.OWED_BY_ME;
|
|
||||||
case NONE -> isPaid ? BadgeMessage.PAID : BadgeMessage.NOT_PAID;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,28 +1,25 @@
|
||||||
package com.application.munera.views;
|
package com.application.munera.views;
|
||||||
|
|
||||||
import com.application.munera.services.CSVService;
|
|
||||||
import com.application.munera.services.ExpenseService;
|
|
||||||
import com.application.munera.services.UserService;
|
|
||||||
import com.application.munera.views.categories.CategoriesView;
|
import com.application.munera.views.categories.CategoriesView;
|
||||||
import com.application.munera.views.dashboard.DashboardView;
|
import com.application.munera.views.dashboard.DashboardView;
|
||||||
import com.application.munera.views.expenses.ExpensesView;
|
import com.application.munera.views.events.EventsView;
|
||||||
|
import com.application.munera.views.expenses.*;
|
||||||
import com.application.munera.views.people.PeopleView;
|
import com.application.munera.views.people.PeopleView;
|
||||||
import com.application.munera.views.settings.SettingsView;
|
|
||||||
import com.application.munera.views.users.UsersView;
|
|
||||||
import com.vaadin.flow.component.applayout.AppLayout;
|
import com.vaadin.flow.component.applayout.AppLayout;
|
||||||
import com.vaadin.flow.component.applayout.DrawerToggle;
|
import com.vaadin.flow.component.applayout.DrawerToggle;
|
||||||
import com.vaadin.flow.component.button.Button;
|
import com.vaadin.flow.component.button.Button;
|
||||||
import com.vaadin.flow.component.html.*;
|
import com.vaadin.flow.component.html.Footer;
|
||||||
|
import com.vaadin.flow.component.html.H1;
|
||||||
|
import com.vaadin.flow.component.html.Header;
|
||||||
|
import com.vaadin.flow.component.html.Span;
|
||||||
import com.vaadin.flow.component.orderedlayout.FlexComponent;
|
import com.vaadin.flow.component.orderedlayout.FlexComponent;
|
||||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||||
import com.vaadin.flow.component.orderedlayout.Scroller;
|
import com.vaadin.flow.component.orderedlayout.Scroller;
|
||||||
import com.vaadin.flow.component.sidenav.SideNav;
|
import com.vaadin.flow.component.sidenav.SideNav;
|
||||||
import com.vaadin.flow.component.sidenav.SideNavItem;
|
import com.vaadin.flow.component.sidenav.SideNavItem;
|
||||||
import com.vaadin.flow.router.PageTitle;
|
import com.vaadin.flow.router.PageTitle;
|
||||||
import com.vaadin.flow.server.StreamResource;
|
|
||||||
import com.vaadin.flow.spring.security.AuthenticationContext;
|
import com.vaadin.flow.spring.security.AuthenticationContext;
|
||||||
import com.vaadin.flow.theme.lumo.LumoUtility;
|
import com.vaadin.flow.theme.lumo.LumoUtility;
|
||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
|
||||||
import org.vaadin.lineawesome.LineAwesomeIcon;
|
import org.vaadin.lineawesome.LineAwesomeIcon;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -31,17 +28,10 @@ import org.vaadin.lineawesome.LineAwesomeIcon;
|
||||||
public class MainLayout extends AppLayout {
|
public class MainLayout extends AppLayout {
|
||||||
|
|
||||||
private H1 viewTitle;
|
private H1 viewTitle;
|
||||||
private Button exportToCSVButton;
|
|
||||||
private final transient AuthenticationContext authContext;
|
private final transient AuthenticationContext authContext;
|
||||||
private final CSVService csvService;
|
|
||||||
private final ExpenseService expenseService;
|
|
||||||
private final UserService userService;
|
|
||||||
|
|
||||||
public MainLayout(AuthenticationContext authContext, CSVService csvService, ExpenseService expenseService, UserService userService) {
|
public MainLayout(AuthenticationContext authContext) {
|
||||||
this.authContext = authContext;
|
this.authContext = authContext;
|
||||||
this.csvService = csvService;
|
|
||||||
this.expenseService = expenseService;
|
|
||||||
this.userService = userService;
|
|
||||||
setPrimarySection(Section.DRAWER);
|
setPrimarySection(Section.DRAWER);
|
||||||
addDrawerContent();
|
addDrawerContent();
|
||||||
addHeaderContent();
|
addHeaderContent();
|
||||||
|
@ -54,52 +44,21 @@ public class MainLayout extends AppLayout {
|
||||||
viewTitle = new H1();
|
viewTitle = new H1();
|
||||||
viewTitle.addClassNames(LumoUtility.FontSize.LARGE, LumoUtility.Margin.NONE);
|
viewTitle.addClassNames(LumoUtility.FontSize.LARGE, LumoUtility.Margin.NONE);
|
||||||
|
|
||||||
// Retrieve the authenticated user's name
|
// Creating the logout button
|
||||||
String username = authContext.getPrincipalName().orElse("Guest");
|
|
||||||
|
|
||||||
// Create the user icon
|
|
||||||
final var userIcon = LineAwesomeIcon.USER.create();
|
|
||||||
userIcon.addClassNames(LumoUtility.FontSize.LARGE); // Make the icon size larger
|
|
||||||
|
|
||||||
// Create a Span to display "Hi, username"
|
|
||||||
Span greeting = new Span("Hi, " + username);
|
|
||||||
greeting.addClassNames(LumoUtility.FontSize.LARGE, LumoUtility.FontWeight.BOLD); // Make text larger and bold
|
|
||||||
|
|
||||||
// Combine the user icon and greeting in a horizontal layout
|
|
||||||
HorizontalLayout userInfoLayout = new HorizontalLayout(userIcon, greeting);
|
|
||||||
userInfoLayout.setAlignItems(FlexComponent.Alignment.CENTER); // Center vertically
|
|
||||||
|
|
||||||
// Create the logout button
|
|
||||||
Button logout = new Button("Logout", click -> this.authContext.logout());
|
Button logout = new Button("Logout", click -> this.authContext.logout());
|
||||||
logout.getStyle().set("padding", "10px"); // Add padding to the logout button
|
|
||||||
|
|
||||||
// Create the Export to CSV button
|
// Adding some padding to the logout button
|
||||||
exportToCSVButton = new Button("Export Expenses to CSV");
|
logout.getStyle().set("padding", "10px");
|
||||||
exportToCSVButton.addClickListener(event -> {
|
|
||||||
// Call the CSV service to create the CSV resource
|
|
||||||
StreamResource resource = this.csvService.createCSVResource(this.expenseService.findAll());
|
|
||||||
resource.setCacheTime(0); // Disable caching to ensure fresh download each time
|
|
||||||
|
|
||||||
// Create a temporary link to trigger the download
|
// Creating the header and adding the logout button to the far left
|
||||||
Anchor downloadLink = new Anchor(resource, "");
|
HorizontalLayout header = new HorizontalLayout(logout);
|
||||||
downloadLink.getElement().setAttribute("download", true);
|
|
||||||
downloadLink.getElement().setAttribute("hidden", true); // Make the link invisible
|
|
||||||
getElement().appendChild(downloadLink.getElement());
|
|
||||||
|
|
||||||
// Programmatically click the link to start the download
|
|
||||||
downloadLink.getElement().callJsFunction("click");
|
|
||||||
});
|
|
||||||
exportToCSVButton.getStyle().set("margin", "0 15px"); // Set margin only for the left and right
|
|
||||||
exportToCSVButton.setVisible(false); // Initially hidden
|
|
||||||
|
|
||||||
// Create the header layout and add all elements
|
|
||||||
HorizontalLayout header = new HorizontalLayout(userInfoLayout, logout);
|
|
||||||
header.setWidthFull(); // Make the header take the full width
|
header.setWidthFull(); // Make the header take the full width
|
||||||
header.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER);
|
header.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER);
|
||||||
header.setJustifyContentMode(FlexComponent.JustifyContentMode.END); // Align items to the right
|
header.setJustifyContentMode(FlexComponent.JustifyContentMode.END); // Align items to the start (left)
|
||||||
header.getStyle().set("padding", "0 10px"); // Add padding around the header
|
header.getStyle().set("padding", "0 10px"); // Add padding around the header if needed
|
||||||
|
|
||||||
addToNavbar(true, toggle, viewTitle, exportToCSVButton);
|
|
||||||
|
addToNavbar(true, toggle, viewTitle);
|
||||||
addToNavbar(header);
|
addToNavbar(header);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,26 +74,18 @@ public class MainLayout extends AppLayout {
|
||||||
|
|
||||||
private SideNav createNavigation() {
|
private SideNav createNavigation() {
|
||||||
SideNav nav = new SideNav();
|
SideNav nav = new SideNav();
|
||||||
// Common menu items
|
|
||||||
nav.addItem(new SideNavItem("Expenses", ExpensesView.class, LineAwesomeIcon.MONEY_BILL_SOLID.create()));
|
nav.addItem(new SideNavItem("Expenses", ExpensesView.class, LineAwesomeIcon.MONEY_BILL_SOLID.create()));
|
||||||
nav.addItem(new SideNavItem("Categories", CategoriesView.class, LineAwesomeIcon.FOLDER.create()));
|
nav.addItem(new SideNavItem("Categories", CategoriesView.class, LineAwesomeIcon.FOLDER.create()));
|
||||||
nav.addItem(new SideNavItem("People", PeopleView.class, LineAwesomeIcon.USER.create()));
|
nav.addItem(new SideNavItem("People", PeopleView.class, LineAwesomeIcon.USER.create()));
|
||||||
|
nav.addItem(new SideNavItem("Events", EventsView.class, LineAwesomeIcon.BANDCAMP.create()));
|
||||||
nav.addItem(new SideNavItem("Dashboard", DashboardView.class, LineAwesomeIcon.CHART_LINE_SOLID.create()));
|
nav.addItem(new SideNavItem("Dashboard", DashboardView.class, LineAwesomeIcon.CHART_LINE_SOLID.create()));
|
||||||
|
|
||||||
// Check user roles before adding sensitive menu items
|
|
||||||
if (isUserAdmin())
|
|
||||||
nav.addItem(new SideNavItem("Users", UsersView.class, LineAwesomeIcon.USER_LOCK_SOLID.create()));
|
|
||||||
|
|
||||||
nav.addItem(new SideNavItem("Settings", SettingsView.class, LineAwesomeIcon.COG_SOLID.create()));
|
|
||||||
return nav;
|
return nav;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isUserAdmin() {
|
|
||||||
final var user = userService.getLoggedInUser();
|
|
||||||
return user.getRoles().contains("ROLE_ADMIN");
|
|
||||||
}
|
|
||||||
|
|
||||||
private Footer createFooter() {
|
private Footer createFooter() {
|
||||||
|
|
||||||
return new Footer();
|
return new Footer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,14 +93,10 @@ public class MainLayout extends AppLayout {
|
||||||
protected void afterNavigation() {
|
protected void afterNavigation() {
|
||||||
super.afterNavigation();
|
super.afterNavigation();
|
||||||
viewTitle.setText(getCurrentPageTitle());
|
viewTitle.setText(getCurrentPageTitle());
|
||||||
|
|
||||||
// Show or hide the Export to CSV button based on the current view
|
|
||||||
boolean isExpensesView = getContent().getClass().equals(ExpensesView.class);
|
|
||||||
exportToCSVButton.setVisible(isExpensesView);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getCurrentPageTitle() {
|
private String getCurrentPageTitle() {
|
||||||
PageTitle title = getContent().getClass().getAnnotation(PageTitle.class);
|
PageTitle title = getContent().getClass().getAnnotation(PageTitle.class);
|
||||||
return title == null ? "" : title.value();
|
return title == null ? "" : title.value();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ package com.application.munera.views.categories;
|
||||||
|
|
||||||
import com.application.munera.data.Category;
|
import com.application.munera.data.Category;
|
||||||
import com.application.munera.services.CategoryService;
|
import com.application.munera.services.CategoryService;
|
||||||
import com.application.munera.services.UserService;
|
|
||||||
import com.application.munera.views.MainLayout;
|
import com.application.munera.views.MainLayout;
|
||||||
import com.vaadin.flow.component.UI;
|
import com.vaadin.flow.component.UI;
|
||||||
import com.vaadin.flow.component.button.Button;
|
import com.vaadin.flow.component.button.Button;
|
||||||
|
@ -21,12 +20,10 @@ import com.vaadin.flow.component.textfield.TextArea;
|
||||||
import com.vaadin.flow.component.textfield.TextField;
|
import com.vaadin.flow.component.textfield.TextField;
|
||||||
import com.vaadin.flow.data.binder.BeanValidationBinder;
|
import com.vaadin.flow.data.binder.BeanValidationBinder;
|
||||||
import com.vaadin.flow.data.binder.ValidationException;
|
import com.vaadin.flow.data.binder.ValidationException;
|
||||||
import com.vaadin.flow.router.BeforeEnterEvent;
|
import com.vaadin.flow.router.*;
|
||||||
import com.vaadin.flow.router.BeforeEnterObserver;
|
import com.vaadin.flow.spring.data.VaadinSpringDataHelpers;
|
||||||
import com.vaadin.flow.router.PageTitle;
|
|
||||||
import com.vaadin.flow.router.Route;
|
|
||||||
import jakarta.annotation.security.PermitAll;
|
import jakarta.annotation.security.PermitAll;
|
||||||
import org.springframework.dao.DataIntegrityViolationException;
|
import org.springframework.data.domain.PageRequest;
|
||||||
import org.springframework.orm.ObjectOptimisticLockingFailureException;
|
import org.springframework.orm.ObjectOptimisticLockingFailureException;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
@ -50,20 +47,20 @@ public class CategoriesView extends Div implements BeforeEnterObserver {
|
||||||
|
|
||||||
private Category category;
|
private Category category;
|
||||||
private final CategoryService categoryService;
|
private final CategoryService categoryService;
|
||||||
private final UserService userService;
|
|
||||||
private TextField name;
|
private TextField name;
|
||||||
|
|
||||||
private TextArea description;
|
private TextArea description;
|
||||||
|
|
||||||
public CategoriesView(CategoryService categoryService, UserService userService) {
|
public CategoriesView(CategoryService categoryService) {
|
||||||
this.categoryService = categoryService;
|
this.categoryService = categoryService;
|
||||||
this.userService = userService;
|
|
||||||
final var userId = this.userService.getLoggedInUser().getId();
|
|
||||||
addClassNames("expenses-view");
|
addClassNames("expenses-view");
|
||||||
|
|
||||||
// Create UI
|
// Create UI
|
||||||
SplitLayout splitLayout = new SplitLayout();
|
SplitLayout splitLayout = new SplitLayout();
|
||||||
|
|
||||||
createGridLayout(splitLayout);
|
createGridLayout(splitLayout);
|
||||||
createEditorLayout(splitLayout);
|
createEditorLayout(splitLayout);
|
||||||
|
|
||||||
add(splitLayout);
|
add(splitLayout);
|
||||||
|
|
||||||
// Configure Grid
|
// Configure Grid
|
||||||
|
@ -71,7 +68,9 @@ public class CategoriesView extends Div implements BeforeEnterObserver {
|
||||||
grid.addColumn(Category::getDescription).setHeader("Description").setSortable(true);
|
grid.addColumn(Category::getDescription).setHeader("Description").setSortable(true);
|
||||||
grid.getColumns().forEach(col -> col.setAutoWidth(true));
|
grid.getColumns().forEach(col -> col.setAutoWidth(true));
|
||||||
|
|
||||||
grid.setItems(this.categoryService.findAllByUserId(userId));
|
grid.setItems(query -> categoryService.list(
|
||||||
|
PageRequest.of(query.getPage(), query.getPageSize(), VaadinSpringDataHelpers.toSpringDataSort(query)))
|
||||||
|
.stream());
|
||||||
grid.addThemeVariants(GridVariant.LUMO_NO_BORDER);
|
grid.addThemeVariants(GridVariant.LUMO_NO_BORDER);
|
||||||
|
|
||||||
// when a row is selected or deselected, populate form
|
// when a row is selected or deselected, populate form
|
||||||
|
@ -87,11 +86,8 @@ public class CategoriesView extends Div implements BeforeEnterObserver {
|
||||||
binder = new BeanValidationBinder<>(Category.class);
|
binder = new BeanValidationBinder<>(Category.class);
|
||||||
|
|
||||||
// Bind fields. This is where you'd define e.g. validation rules
|
// Bind fields. This is where you'd define e.g. validation rules
|
||||||
binder.bindInstanceFields(this);
|
|
||||||
binder.forField(name)
|
|
||||||
.asRequired("Name is required")
|
|
||||||
.bind(Category::getName, Category::setName);
|
|
||||||
|
|
||||||
|
binder.bindInstanceFields(this);
|
||||||
|
|
||||||
cancel.addClickListener(e -> {
|
cancel.addClickListener(e -> {
|
||||||
clearForm();
|
clearForm();
|
||||||
|
@ -104,7 +100,7 @@ public class CategoriesView extends Div implements BeforeEnterObserver {
|
||||||
this.category = new Category();
|
this.category = new Category();
|
||||||
}
|
}
|
||||||
binder.writeBean(this.category);
|
binder.writeBean(this.category);
|
||||||
categoryService.update(this.category, userId);
|
categoryService.update(this.category);
|
||||||
clearForm();
|
clearForm();
|
||||||
refreshGrid();
|
refreshGrid();
|
||||||
Notification.show("Data updated");
|
Notification.show("Data updated");
|
||||||
|
@ -115,13 +111,13 @@ public class CategoriesView extends Div implements BeforeEnterObserver {
|
||||||
n.setPosition(Notification.Position.MIDDLE);
|
n.setPosition(Notification.Position.MIDDLE);
|
||||||
n.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
n.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||||
} catch (ValidationException validationException) {
|
} catch (ValidationException validationException) {
|
||||||
Notification.show("Failed to update the category. Check again that all values are valid");
|
Notification.show("Failed to update the data. Check again that all values are valid");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
delete.addClickListener(e -> {
|
delete.addClickListener(e -> {
|
||||||
try {
|
try {
|
||||||
if (this.category == null) throw new IllegalStateException("Category is null!");
|
if (this.category == null) throw new RuntimeException("Category is null!"); //TODO: create proper exception
|
||||||
categoryService.delete(this.category);
|
categoryService.delete(this.category);
|
||||||
clearForm();
|
clearForm();
|
||||||
refreshGrid();
|
refreshGrid();
|
||||||
|
@ -132,11 +128,6 @@ public class CategoriesView extends Div implements BeforeEnterObserver {
|
||||||
"Error updating the data. Somebody else has updated the record while you were making changes.");
|
"Error updating the data. Somebody else has updated the record while you were making changes.");
|
||||||
n.setPosition(Notification.Position.MIDDLE);
|
n.setPosition(Notification.Position.MIDDLE);
|
||||||
n.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
n.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||||
} catch (DataIntegrityViolationException ex) {
|
|
||||||
Notification n = Notification.show(
|
|
||||||
"Cannot delete this category as it is associated with existing expenses.");
|
|
||||||
n.setPosition(Notification.Position.MIDDLE);
|
|
||||||
n.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,13 +2,10 @@ package com.application.munera.views.dashboard;
|
||||||
|
|
||||||
import com.application.munera.data.Expense;
|
import com.application.munera.data.Expense;
|
||||||
import com.application.munera.data.Person;
|
import com.application.munera.data.Person;
|
||||||
import com.application.munera.data.User;
|
|
||||||
import com.application.munera.facades.PersonFacade;
|
|
||||||
import com.application.munera.services.ExpenseService;
|
import com.application.munera.services.ExpenseService;
|
||||||
import com.application.munera.services.UserService;
|
import com.application.munera.services.PersonService;
|
||||||
import com.application.munera.views.MainLayout;
|
import com.application.munera.views.MainLayout;
|
||||||
import com.nimbusds.jose.shaded.gson.Gson;
|
import com.nimbusds.jose.shaded.gson.Gson;
|
||||||
import com.vaadin.flow.component.combobox.ComboBox;
|
|
||||||
import com.vaadin.flow.component.html.Div;
|
import com.vaadin.flow.component.html.Div;
|
||||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||||
|
@ -17,7 +14,6 @@ import com.vaadin.flow.router.Route;
|
||||||
import jakarta.annotation.security.PermitAll;
|
import jakarta.annotation.security.PermitAll;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.Month;
|
|
||||||
import java.time.Year;
|
import java.time.Year;
|
||||||
import java.time.YearMonth;
|
import java.time.YearMonth;
|
||||||
import java.time.format.TextStyle;
|
import java.time.format.TextStyle;
|
||||||
|
@ -31,42 +27,22 @@ import java.util.stream.Collectors;
|
||||||
public class DashboardView extends Div {
|
public class DashboardView extends Div {
|
||||||
|
|
||||||
private final ExpenseService expenseService;
|
private final ExpenseService expenseService;
|
||||||
private final PersonFacade personFacade;
|
private final PersonService personService;
|
||||||
private final User loggedUser;
|
|
||||||
private final Person loggedPerson;
|
|
||||||
private final ComboBox<Integer> yearComboBox;
|
|
||||||
|
|
||||||
public DashboardView(ExpenseService expenseService, UserService userService, PersonFacade personFacade) {
|
public DashboardView(final ExpenseService expenseService, final PersonService personService) {
|
||||||
this.expenseService = expenseService;
|
this.expenseService = expenseService;
|
||||||
this.personFacade = personFacade;
|
this.personService = personService;
|
||||||
loggedUser = userService.getLoggedInUser();
|
|
||||||
loggedPerson = personFacade.getLoggedInPerson();
|
|
||||||
addClassName("highcharts-view"); // Optional CSS class for styling
|
addClassName("highcharts-view"); // Optional CSS class for styling
|
||||||
|
|
||||||
// Fetch available years from the database
|
|
||||||
List<Integer> availableYears = this.expenseService.getAvailableExpenseYearsForUser(loggedUser.getId());
|
|
||||||
|
|
||||||
// Initialize the ComboBox for year selection
|
|
||||||
yearComboBox = new ComboBox<>();
|
|
||||||
yearComboBox.setItems(availableYears);
|
|
||||||
yearComboBox.setValue(Year.now().getValue()); // Default to current year
|
|
||||||
yearComboBox.setWidth("200px");
|
|
||||||
|
|
||||||
// Add listener to update charts when a new year is selected
|
|
||||||
yearComboBox.addValueChangeListener(event -> {
|
|
||||||
if (event.getValue() != null) updateCharts(Year.of(event.getValue()));
|
|
||||||
});
|
|
||||||
|
|
||||||
VerticalLayout mainLayout = new VerticalLayout();
|
VerticalLayout mainLayout = new VerticalLayout();
|
||||||
mainLayout.add(yearComboBox);
|
|
||||||
mainLayout.setSizeFull();
|
mainLayout.setSizeFull();
|
||||||
mainLayout.getStyle().set("padding", "5px"); // Add padding to main layout
|
mainLayout.getStyle().set("padding", "10px"); // Add padding to main layout
|
||||||
|
|
||||||
// Create a horizontal layout for the top row
|
// Create a horizontal layout for the top row
|
||||||
HorizontalLayout topRowLayout = new HorizontalLayout();
|
HorizontalLayout topRowLayout = new HorizontalLayout();
|
||||||
topRowLayout.setSizeFull();
|
topRowLayout.setSizeFull();
|
||||||
topRowLayout.setHeight("50%"); // Make sure the top row occupies half of the page height
|
topRowLayout.setHeight("50%"); // Make sure the top row occupies half of the page height
|
||||||
topRowLayout.getStyle().set("padding", "5px"); // Add padding to top row
|
topRowLayout.getStyle().set("padding", "10px"); // Add padding to top row
|
||||||
|
|
||||||
// Create and add the existing bar chart to the top left
|
// Create and add the existing bar chart to the top left
|
||||||
Div barChartDiv = new Div();
|
Div barChartDiv = new Div();
|
||||||
|
@ -92,7 +68,7 @@ public class DashboardView extends Div {
|
||||||
HorizontalLayout bottomRowLayout = new HorizontalLayout();
|
HorizontalLayout bottomRowLayout = new HorizontalLayout();
|
||||||
bottomRowLayout.setSizeFull();
|
bottomRowLayout.setSizeFull();
|
||||||
bottomRowLayout.setHeight("50%"); // Make sure the bottom row occupies the other half of the page height
|
bottomRowLayout.setHeight("50%"); // Make sure the bottom row occupies the other half of the page height
|
||||||
bottomRowLayout.getStyle().set("padding", "5px"); // Add padding to bottom row
|
bottomRowLayout.getStyle().set("padding", "10px"); // Add padding to bottom row
|
||||||
|
|
||||||
// Create the bottom left chart
|
// Create the bottom left chart
|
||||||
Div bottomLeftChartDiv = new Div();
|
Div bottomLeftChartDiv = new Div();
|
||||||
|
@ -113,119 +89,68 @@ public class DashboardView extends Div {
|
||||||
bottomRowLayout.add(bottomRightChartDiv);
|
bottomRowLayout.add(bottomRightChartDiv);
|
||||||
|
|
||||||
mainLayout.add(bottomRowLayout);
|
mainLayout.add(bottomRowLayout);
|
||||||
|
|
||||||
add(mainLayout);
|
add(mainLayout);
|
||||||
updateCharts(Year.now());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the charts based on the selected year
|
String barChartJs = generateBarChartScript();
|
||||||
private void updateCharts(Year year) {
|
String pieChartJs = generatePieChartScript();
|
||||||
String barChartJs = generateBarChartScript(year);
|
String bottomLeftChartJs = generateNegativeColumnChartScript();
|
||||||
String pieChartJs = generatePieChartScript(year);
|
String bottomRightChartJs = generateExpensesOverTimeByCategoryScript();
|
||||||
String negativeColumnChartJs = generateNegativeColumnChartScript();
|
|
||||||
String expensesOverTimeByCategoryChart = generateExpensesOverTimeByCategoryScript(year);
|
|
||||||
|
|
||||||
// Execute the JavaScript to update the charts
|
// Execute the JavaScript to initialize the charts
|
||||||
getElement().executeJs(barChartJs);
|
getElement().executeJs(barChartJs);
|
||||||
getElement().executeJs(pieChartJs);
|
getElement().executeJs(pieChartJs);
|
||||||
getElement().executeJs(negativeColumnChartJs);
|
getElement().executeJs(bottomLeftChartJs);
|
||||||
getElement().executeJs(expensesOverTimeByCategoryChart);
|
getElement().executeJs(bottomRightChartJs);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String generateBarChartScript(Year year) {
|
private String generateBarChartScript() {
|
||||||
List<Expense> expenses = expenseService.fetchExpensesForDashboard(loggedPerson, year);
|
List<Expense> expenses = expenseService.findAllByYear(Year.now().getValue());
|
||||||
|
|
||||||
// Create a map to store data by month and category
|
// Prepare data for Highcharts
|
||||||
Map<String, Map<String, Double>> monthlyCategoryData = new LinkedHashMap<>();
|
Map<String, Double> monthlyData = new LinkedHashMap<>();
|
||||||
|
YearMonth currentYearMonth = YearMonth.now().withMonth(1); // Start from January
|
||||||
|
|
||||||
// Initialize all months (from January to December) for each category
|
|
||||||
List<String> monthNames = new ArrayList<>();
|
|
||||||
for (int i = 1; i <= 12; i++) {
|
for (int i = 1; i <= 12; i++) {
|
||||||
String monthName = Month.of(i).getDisplayName(TextStyle.SHORT, Locale.ENGLISH);
|
String monthName = currentYearMonth.getMonth().getDisplayName(TextStyle.SHORT, Locale.ENGLISH);
|
||||||
monthNames.add(monthName);
|
monthlyData.put(monthName, 0.0);
|
||||||
monthlyCategoryData.putIfAbsent(monthName, new LinkedHashMap<>());
|
currentYearMonth = currentYearMonth.plusMonths(1); // Move to the next month
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate the map with actual expense data
|
// Populate map with actual data
|
||||||
for (Expense expense : expenses) {
|
for (Expense expense : expenses) {
|
||||||
String monthName = expense.getDate().getMonth().getDisplayName(TextStyle.SHORT, Locale.ENGLISH);
|
String monthName = expense.getDate().getMonth().getDisplayName(TextStyle.SHORT, Locale.ENGLISH);
|
||||||
String categoryName = expense.getCategory().getName();
|
|
||||||
Double amount = expense.getCost().doubleValue();
|
Double amount = expense.getCost().doubleValue();
|
||||||
|
monthlyData.put(monthName, monthlyData.get(monthName) + amount);
|
||||||
monthlyCategoryData.putIfAbsent(monthName, new LinkedHashMap<>());
|
|
||||||
Map<String, Double> categoryData = monthlyCategoryData.get(monthName);
|
|
||||||
categoryData.put(categoryName, categoryData.getOrDefault(categoryName, 0.0) + amount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare series data for Highcharts, with each category being a separate series
|
// Prepare series data for Highcharts
|
||||||
Map<String, Map<String, Double>> categoryMonthlyData = new LinkedHashMap<>();
|
StringBuilder data = new StringBuilder("[");
|
||||||
for (String monthName : monthNames) {
|
for (Map.Entry<String, Double> entry : monthlyData.entrySet()) {
|
||||||
Map<String, Double> monthData = monthlyCategoryData.getOrDefault(monthName, new LinkedHashMap<>());
|
data.append(entry.getValue()).append(",");
|
||||||
for (Map.Entry<String, Double> entry : monthData.entrySet()) {
|
|
||||||
String categoryName = entry.getKey();
|
|
||||||
Double amount = entry.getValue();
|
|
||||||
|
|
||||||
categoryMonthlyData.putIfAbsent(categoryName, new LinkedHashMap<>());
|
|
||||||
categoryMonthlyData.get(categoryName).put(monthName, amount);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
data.setCharAt(data.length() - 1, ']'); // Replace last comma with closing bracket
|
||||||
|
|
||||||
// Build the series data for each category
|
// Generate JavaScript initialization
|
||||||
StringBuilder seriesData = new StringBuilder("[");
|
|
||||||
for (Map.Entry<String, Map<String, Double>> entry : categoryMonthlyData.entrySet()) {
|
|
||||||
String categoryName = entry.getKey();
|
|
||||||
Map<String, Double> monthData = entry.getValue();
|
|
||||||
|
|
||||||
seriesData.append("{");
|
|
||||||
seriesData.append("name: '").append(categoryName).append("',");
|
|
||||||
seriesData.append("data: [");
|
|
||||||
|
|
||||||
for (String monthName : monthNames) {
|
|
||||||
seriesData.append(monthData.getOrDefault(monthName, 0.0)).append(",");
|
|
||||||
}
|
|
||||||
|
|
||||||
seriesData.setLength(seriesData.length() - 1); // Remove trailing comma
|
|
||||||
seriesData.append("], stack: 'expenses'");
|
|
||||||
seriesData.append("},");
|
|
||||||
}
|
|
||||||
seriesData.setLength(seriesData.length() - 1); // Remove trailing comma
|
|
||||||
seriesData.append("]");
|
|
||||||
|
|
||||||
// Generate the JavaScript for the stacked column chart
|
|
||||||
return "Highcharts.chart('barChart', {" +
|
return "Highcharts.chart('barChart', {" +
|
||||||
"chart: { type: 'column' }, " +
|
"chart: {" +
|
||||||
"title: { text: 'Monthly Expenses by Category for " + Year.now().getValue() + "' }, " +
|
"type: 'column'" +
|
||||||
"xAxis: { categories: " + new Gson().toJson(monthNames) + " }, " +
|
|
||||||
"yAxis: { " +
|
|
||||||
"min: 0, " +
|
|
||||||
"title: { text: 'Total Expense' }, " +
|
|
||||||
"stackLabels: { " +
|
|
||||||
"enabled: true, " +
|
|
||||||
"style: { fontWeight: 'bold', fontSize: '14px', color: 'white', textOutline: '1px contrast', " +
|
|
||||||
"backgroundColor: 'rgba(0,0,0,0.75)', padding: 4, borderRadius: 3 }, " + // Background and border for better visibility
|
|
||||||
"formatter: function() { " +
|
|
||||||
" if (this.total > 0) { return this.total; } else { return ''; }" + // Only show total if greater than 0
|
|
||||||
"}" +
|
|
||||||
"} " +
|
|
||||||
"}, " +
|
|
||||||
"plotOptions: { " +
|
|
||||||
"column: { " +
|
|
||||||
"stacking: 'normal', " +
|
|
||||||
"dataLabels: { " +
|
|
||||||
"enabled: true, " +
|
|
||||||
"formatter: function() { " +
|
|
||||||
" if (this.y > 0) { return this.y; } else { return ''; }" + // Only show data label if value > 0
|
|
||||||
"}," +
|
"}," +
|
||||||
"color: 'black', " + // Set data label color to black
|
"title: {" +
|
||||||
"style: { fontSize: '12px', fontWeight: 'bold' }" + // Customize individual labels' appearance
|
"text: 'Monthly Expenses for " + Year.now().getValue() + "'" +
|
||||||
"} " +
|
"}," +
|
||||||
"} " +
|
"xAxis: {" +
|
||||||
"}, " +
|
"categories: " + new Gson().toJson(monthlyData.keySet()) + // Categories are the month names
|
||||||
"series: " + seriesData + " " +
|
"}," +
|
||||||
|
"series: [{" +
|
||||||
|
"name: 'Expenses'," +
|
||||||
|
"data: " + data + // Use the data fetched from DB
|
||||||
|
"}]" +
|
||||||
"});";
|
"});";
|
||||||
}
|
}
|
||||||
|
|
||||||
private String generatePieChartScript(Year year) {
|
private String generatePieChartScript() {
|
||||||
List<Expense> expenses = expenseService.fetchExpensesForDashboard(loggedPerson, year);
|
List<Expense> expenses = expenseService.findAllByYear(Year.now().getValue());
|
||||||
|
|
||||||
// Group expenses by category name and sum their costs
|
// Group expenses by category name and sum their costs
|
||||||
Map<String, Double> categoryData = expenses.stream()
|
Map<String, Double> categoryData = expenses.stream()
|
||||||
|
@ -242,7 +167,7 @@ public class DashboardView extends Div {
|
||||||
}
|
}
|
||||||
data.setCharAt(data.length() - 1, ']'); // Replace last comma with closing bracket
|
data.setCharAt(data.length() - 1, ']'); // Replace last comma with closing bracket
|
||||||
|
|
||||||
// Generate JavaScript initialization with percentage tooltip
|
// Generate JavaScript initialization
|
||||||
return "Highcharts.chart('pieChart', {" +
|
return "Highcharts.chart('pieChart', {" +
|
||||||
"chart: {" +
|
"chart: {" +
|
||||||
"type: 'pie'" +
|
"type: 'pie'" +
|
||||||
|
@ -250,16 +175,9 @@ public class DashboardView extends Div {
|
||||||
"title: {" +
|
"title: {" +
|
||||||
"text: 'Expenses by Category for " + Year.now().getValue() + "'" +
|
"text: 'Expenses by Category for " + Year.now().getValue() + "'" +
|
||||||
"}," +
|
"}," +
|
||||||
"tooltip: {" + // Tooltip configuration to show percentage
|
|
||||||
"pointFormat: '{series.name}: <b>{point.percentage:.1f}%</b> ({point.y:.2f})'" +
|
|
||||||
"}," +
|
|
||||||
"plotOptions: {" +
|
"plotOptions: {" +
|
||||||
"pie: {" +
|
"pie: {" +
|
||||||
"size: '80%'," + // Adjust size to make the pie chart larger
|
"size: '80%'" + // Adjust size to make the pie chart larger
|
||||||
"dataLabels: {" + // Enable data labels
|
|
||||||
"enabled: true," +
|
|
||||||
"format: '{point.name}: {point.percentage:.1f}%'" + // Show percentage next to each slice
|
|
||||||
"}" +
|
|
||||||
"}" +
|
"}" +
|
||||||
"}," +
|
"}," +
|
||||||
"series: [{" +
|
"series: [{" +
|
||||||
|
@ -271,22 +189,17 @@ public class DashboardView extends Div {
|
||||||
}
|
}
|
||||||
|
|
||||||
private String generateNegativeColumnChartScript() {
|
private String generateNegativeColumnChartScript() {
|
||||||
final var people = personFacade.findAllExcludeLoggedUser(loggedUser);
|
final var people = personService.findAll().stream()
|
||||||
// Create a map to store person names and their balances
|
.filter(person -> personService.calculateNetBalance(person).compareTo(BigDecimal.ZERO) != 0)
|
||||||
Map<String, Double> personData = people.stream()
|
.toList();
|
||||||
.map(person -> {
|
if (people.isEmpty()) return generatePlaceholderChartScript("bottomLeftChart", "No Data Available");
|
||||||
BigDecimal balance = personFacade.calculateNetBalance(person);
|
|
||||||
// Return an array with the person’s first name and the balance
|
Map<String, Double> personData = new LinkedHashMap<>();
|
||||||
return new Object[]{person.getFirstName(), balance};
|
|
||||||
})
|
for (Person person : people) {
|
||||||
.filter(entry -> ((BigDecimal) entry[1]).compareTo(BigDecimal.ZERO) != 0)
|
BigDecimal balance = personService.calculateNetBalance(person);
|
||||||
.collect(Collectors.toMap(
|
personData.put(person.getFirstName(), balance.doubleValue());
|
||||||
entry -> (String) entry[0],
|
}
|
||||||
entry -> ((BigDecimal) entry[1]).negate().doubleValue(),
|
|
||||||
(existing, replacement) -> existing,
|
|
||||||
LinkedHashMap::new // Preserves insertion order
|
|
||||||
));
|
|
||||||
if (personData.isEmpty()) return generatePlaceholderChartScript("bottomLeftChart", "All Payments Settled");
|
|
||||||
|
|
||||||
// Prepare series data for Highcharts with conditional coloring
|
// Prepare series data for Highcharts with conditional coloring
|
||||||
StringBuilder data = new StringBuilder("[");
|
StringBuilder data = new StringBuilder("[");
|
||||||
|
@ -295,18 +208,18 @@ public class DashboardView extends Div {
|
||||||
String color = value >= 0 ? "#90EE90" : "#FF9999"; // Green for positive, red for negative
|
String color = value >= 0 ? "#90EE90" : "#FF9999"; // Green for positive, red for negative
|
||||||
data.append("{ y: ").append(value).append(", color: '").append(color).append("' },");
|
data.append("{ y: ").append(value).append(", color: '").append(color).append("' },");
|
||||||
}
|
}
|
||||||
data.setCharAt(data.length() - 1, ']'); // Replace the last comma with a closing bracket
|
data.setCharAt(data.length() - 1, ']'); // Replace last comma with closing bracket
|
||||||
|
|
||||||
// Generate JavaScript initialization
|
// Generate JavaScript initialization
|
||||||
return "Highcharts.chart('bottomLeftChart', {" +
|
return "Highcharts.chart('bottomLeftChart', {" +
|
||||||
"chart: {" +
|
"chart: {" +
|
||||||
"type: 'column'," +
|
"type: 'column'," + // Specify the chart type as column
|
||||||
"}," +
|
"}," +
|
||||||
"title: {" +
|
"title: {" +
|
||||||
"text: 'Net Balances by Person'" +
|
"text: 'Net Balances by Person'" +
|
||||||
"}," +
|
"}," +
|
||||||
"xAxis: {" +
|
"xAxis: {" +
|
||||||
"categories: " + new Gson().toJson(personData.keySet()) +
|
"categories: " + new Gson().toJson(personData.keySet()) + // Categories are the person names
|
||||||
"}," +
|
"}," +
|
||||||
"yAxis: {" +
|
"yAxis: {" +
|
||||||
"title: {" +
|
"title: {" +
|
||||||
|
@ -318,25 +231,16 @@ public class DashboardView extends Div {
|
||||||
"color: '#808080'" +
|
"color: '#808080'" +
|
||||||
"}]" +
|
"}]" +
|
||||||
"}," +
|
"}," +
|
||||||
"plotOptions: {" +
|
"plotOptions: {" + // Add plotOptions to configure the column width
|
||||||
"column: {" +
|
"column: {" +
|
||||||
"pointWidth: 50," +
|
"pointWidth: 50" + // Adjust the width of the columns (in pixels)
|
||||||
"threshold: 0" +
|
|
||||||
"}" +
|
|
||||||
"}," +
|
|
||||||
"tooltip: {" + // Combine default point format with a custom label
|
|
||||||
"useHTML: true," +
|
|
||||||
"formatter: function() {" +
|
|
||||||
"var label = this.y >= 0 ? 'Credit: ' : 'Debit: '; " +
|
|
||||||
"return '<span style=\"color:' + this.point.color + '\">\u25CF</span> ' + '<b>' + this.x + '</b><br/>' + label + Math.abs(this.y);" +
|
|
||||||
"}" +
|
"}" +
|
||||||
"}," +
|
"}," +
|
||||||
"series: [{" +
|
"series: [{" +
|
||||||
"name: 'Balance'," +
|
"name: 'Balance'," +
|
||||||
"data: " + data +
|
"data: " + data + // Use the data fetched from DB
|
||||||
"}]" +
|
"}]" +
|
||||||
"});";
|
"});"; }
|
||||||
}
|
|
||||||
|
|
||||||
private String generatePlaceholderChartScript(String divId, String title) {
|
private String generatePlaceholderChartScript(String divId, String title) {
|
||||||
return "Highcharts.chart('" + divId + "', {" +
|
return "Highcharts.chart('" + divId + "', {" +
|
||||||
|
@ -353,8 +257,8 @@ public class DashboardView extends Div {
|
||||||
"});";
|
"});";
|
||||||
}
|
}
|
||||||
|
|
||||||
private String generateExpensesOverTimeByCategoryScript(Year year) {
|
private String generateExpensesOverTimeByCategoryScript() {
|
||||||
List<Expense> expenses = expenseService.fetchExpensesForDashboard(loggedPerson, year);
|
List<Expense> expenses = expenseService.findAllByYear(Year.now().getValue());
|
||||||
|
|
||||||
// Group expenses by category and by month
|
// Group expenses by category and by month
|
||||||
Map<String, Map<String, Double>> categoryMonthlyData = new LinkedHashMap<>();
|
Map<String, Map<String, Double>> categoryMonthlyData = new LinkedHashMap<>();
|
||||||
|
|
|
@ -0,0 +1,213 @@
|
||||||
|
package com.application.munera.views.events;
|
||||||
|
|
||||||
|
import com.application.munera.data.Event;
|
||||||
|
import com.application.munera.data.Person;
|
||||||
|
import com.application.munera.services.EventService;
|
||||||
|
import com.application.munera.services.PersonService;
|
||||||
|
import com.application.munera.views.MainLayout;
|
||||||
|
import com.vaadin.flow.component.UI;
|
||||||
|
import com.vaadin.flow.component.button.Button;
|
||||||
|
import com.vaadin.flow.component.button.ButtonVariant;
|
||||||
|
import com.vaadin.flow.component.combobox.MultiSelectComboBox;
|
||||||
|
import com.vaadin.flow.component.dependency.Uses;
|
||||||
|
import com.vaadin.flow.component.formlayout.FormLayout;
|
||||||
|
import com.vaadin.flow.component.grid.Grid;
|
||||||
|
import com.vaadin.flow.component.grid.GridVariant;
|
||||||
|
import com.vaadin.flow.component.html.Div;
|
||||||
|
import com.vaadin.flow.component.icon.Icon;
|
||||||
|
import com.vaadin.flow.component.notification.Notification;
|
||||||
|
import com.vaadin.flow.component.notification.NotificationVariant;
|
||||||
|
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||||
|
import com.vaadin.flow.component.splitlayout.SplitLayout;
|
||||||
|
import com.vaadin.flow.component.textfield.TextArea;
|
||||||
|
import com.vaadin.flow.component.textfield.TextField;
|
||||||
|
import com.vaadin.flow.data.binder.BeanValidationBinder;
|
||||||
|
import com.vaadin.flow.data.binder.ValidationException;
|
||||||
|
import com.vaadin.flow.router.BeforeEnterEvent;
|
||||||
|
import com.vaadin.flow.router.BeforeEnterObserver;
|
||||||
|
import com.vaadin.flow.router.PageTitle;
|
||||||
|
import com.vaadin.flow.router.Route;
|
||||||
|
import com.vaadin.flow.spring.data.VaadinSpringDataHelpers;
|
||||||
|
import jakarta.annotation.security.PermitAll;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.orm.ObjectOptimisticLockingFailureException;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@PageTitle("Events")
|
||||||
|
@PermitAll
|
||||||
|
@Route(value = "events/:eventID?/:action?(edit)", layout = MainLayout.class)
|
||||||
|
@Uses(Icon.class)
|
||||||
|
public class EventsView extends Div implements BeforeEnterObserver {
|
||||||
|
|
||||||
|
private static final String EVENT_ID = "eventID";
|
||||||
|
private static final String EVENT_EDIT_ROUTE_TEMPLATE = "events/%s/edit";
|
||||||
|
|
||||||
|
private final Grid<Event> grid = new Grid<>(Event.class, false);
|
||||||
|
|
||||||
|
private final Button cancel = new Button("Cancel");
|
||||||
|
private final Button save = new Button("Save");
|
||||||
|
private final Button delete = new Button("Delete");
|
||||||
|
|
||||||
|
private final BeanValidationBinder<Event> binder;
|
||||||
|
|
||||||
|
private Event event;
|
||||||
|
private final EventService eventService;
|
||||||
|
private final PersonService personService;
|
||||||
|
private TextField name;
|
||||||
|
private TextArea description;
|
||||||
|
private MultiSelectComboBox<Person> participants;
|
||||||
|
|
||||||
|
public EventsView(EventService eventService, PersonService personService) {
|
||||||
|
this.eventService = eventService;
|
||||||
|
this.personService = personService;
|
||||||
|
addClassNames("expenses-view");
|
||||||
|
|
||||||
|
// Create UI
|
||||||
|
SplitLayout splitLayout = new SplitLayout();
|
||||||
|
|
||||||
|
createGridLayout(splitLayout);
|
||||||
|
createEditorLayout(splitLayout);
|
||||||
|
|
||||||
|
add(splitLayout);
|
||||||
|
|
||||||
|
// Configure Grid
|
||||||
|
grid.addColumn(Event::getName).setHeader("Name").setSortable(true);
|
||||||
|
grid.addColumn(Event::getDescription).setHeader("Description").setSortable(true);
|
||||||
|
grid.getColumns().forEach(col -> col.setAutoWidth(true));
|
||||||
|
|
||||||
|
grid.setItems(query -> eventService.list(
|
||||||
|
PageRequest.of(query.getPage(), query.getPageSize(), VaadinSpringDataHelpers.toSpringDataSort(query)))
|
||||||
|
.stream());
|
||||||
|
grid.addThemeVariants(GridVariant.LUMO_NO_BORDER);
|
||||||
|
|
||||||
|
// when a row is selected or deselected, populate form
|
||||||
|
grid.asSingleSelect().addValueChangeListener(event -> {
|
||||||
|
if (event.getValue() != null) UI.getCurrent().navigate(String.format(EVENT_EDIT_ROUTE_TEMPLATE, event.getValue().getId()));
|
||||||
|
else {
|
||||||
|
clearForm();
|
||||||
|
UI.getCurrent().navigate(EventsView.class);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure Form
|
||||||
|
binder = new BeanValidationBinder<>(Event.class);
|
||||||
|
|
||||||
|
// Bind fields. This is where you'd define e.g. validation rules
|
||||||
|
|
||||||
|
binder.bindInstanceFields(this);
|
||||||
|
|
||||||
|
cancel.addClickListener(e -> {
|
||||||
|
clearForm();
|
||||||
|
refreshGrid();
|
||||||
|
});
|
||||||
|
|
||||||
|
save.addClickListener(e -> {
|
||||||
|
try {
|
||||||
|
if (this.event == null) {
|
||||||
|
this.event = new Event();
|
||||||
|
}
|
||||||
|
binder.writeBean(this.event);
|
||||||
|
eventService.update(this.event);
|
||||||
|
clearForm();
|
||||||
|
refreshGrid();
|
||||||
|
Notification.show("Data updated");
|
||||||
|
UI.getCurrent().navigate(EventsView.class);
|
||||||
|
} catch (ObjectOptimisticLockingFailureException exception) {
|
||||||
|
Notification n = Notification.show(
|
||||||
|
"Error updating the data. Somebody else has updated the record while you were making changes.");
|
||||||
|
n.setPosition(Notification.Position.MIDDLE);
|
||||||
|
n.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||||
|
} catch (ValidationException validationException) {
|
||||||
|
Notification.show("Failed to update the data. Check again that all values are valid");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
delete.addClickListener(e -> {
|
||||||
|
try {
|
||||||
|
if (this.event == null) throw new RuntimeException("Event is null!"); //TODO: create proper exception
|
||||||
|
eventService.delete(this.event);
|
||||||
|
clearForm();
|
||||||
|
refreshGrid();
|
||||||
|
Notification.show("Data deleted");
|
||||||
|
UI.getCurrent().navigate(EventsView.class);
|
||||||
|
} catch (ObjectOptimisticLockingFailureException exception) {
|
||||||
|
Notification n = Notification.show(
|
||||||
|
"Error updating the data. Somebody else has updated the record while you were making changes.");
|
||||||
|
n.setPosition(Notification.Position.MIDDLE);
|
||||||
|
n.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeEnter(BeforeEnterEvent event) {
|
||||||
|
Optional<Long> eventId = event.getRouteParameters().get(EVENT_ID).map(Long::parseLong);
|
||||||
|
if (eventId.isPresent()) {
|
||||||
|
Optional<Event> eventFromBackend = eventService.findById(eventId.get());
|
||||||
|
if (eventFromBackend.isPresent()) {
|
||||||
|
populateForm(eventFromBackend.get());
|
||||||
|
} else {
|
||||||
|
Notification.show(
|
||||||
|
String.format("The requested event was not found, ID = %s", eventId.get()), 3000,
|
||||||
|
Notification.Position.BOTTOM_START);
|
||||||
|
// when a row is selected but the data is no longer available,
|
||||||
|
// refresh grid
|
||||||
|
refreshGrid();
|
||||||
|
event.forwardTo(EventsView.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createEditorLayout(SplitLayout splitLayout) {
|
||||||
|
Div editorLayoutDiv = new Div();
|
||||||
|
editorLayoutDiv.setClassName("editor-layout");
|
||||||
|
|
||||||
|
Div editorDiv = new Div();
|
||||||
|
editorDiv.setClassName("editor");
|
||||||
|
editorLayoutDiv.add(editorDiv);
|
||||||
|
|
||||||
|
FormLayout formLayout = new FormLayout();
|
||||||
|
name = new TextField("Name");
|
||||||
|
description = new TextArea("Description");
|
||||||
|
participants = new MultiSelectComboBox<>("Participants");
|
||||||
|
participants.setItems(personService.findAll());
|
||||||
|
participants.setItemLabelGenerator(Person::getFirstName);
|
||||||
|
formLayout.add(name, description, participants);
|
||||||
|
editorDiv.add(formLayout);
|
||||||
|
createButtonLayout(editorLayoutDiv);
|
||||||
|
|
||||||
|
splitLayout.addToSecondary(editorLayoutDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createButtonLayout(Div editorLayoutDiv) {
|
||||||
|
HorizontalLayout buttonLayout = new HorizontalLayout();
|
||||||
|
buttonLayout.setClassName("button-layout");
|
||||||
|
cancel.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||||
|
save.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||||
|
delete.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||||
|
buttonLayout.add(save, delete, cancel);
|
||||||
|
editorLayoutDiv.add(buttonLayout);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createGridLayout(SplitLayout splitLayout) {
|
||||||
|
Div wrapper = new Div();
|
||||||
|
wrapper.setClassName("grid-wrapper");
|
||||||
|
splitLayout.addToPrimary(wrapper);
|
||||||
|
wrapper.add(grid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshGrid() {
|
||||||
|
grid.select(null);
|
||||||
|
grid.getDataProvider().refreshAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearForm() {
|
||||||
|
populateForm(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void populateForm(Event value) {
|
||||||
|
this.event = value;
|
||||||
|
binder.readBean(this.event);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +1,10 @@
|
||||||
package com.application.munera.views.expenses;
|
package com.application.munera.views.expenses;
|
||||||
|
|
||||||
import com.application.munera.data.Category;
|
import com.application.munera.data.*;
|
||||||
import com.application.munera.data.Expense;
|
|
||||||
import com.application.munera.data.enums.PeriodUnit;
|
|
||||||
import com.application.munera.data.Person;
|
|
||||||
import com.application.munera.facades.PersonFacade;
|
|
||||||
import com.application.munera.services.CategoryService;
|
import com.application.munera.services.CategoryService;
|
||||||
|
import com.application.munera.services.EventService;
|
||||||
import com.application.munera.services.ExpenseService;
|
import com.application.munera.services.ExpenseService;
|
||||||
import com.application.munera.services.UserService;
|
import com.application.munera.services.PersonService;
|
||||||
import com.application.munera.services.ViewsService;
|
|
||||||
import com.application.munera.views.MainLayout;
|
import com.application.munera.views.MainLayout;
|
||||||
import com.vaadin.flow.component.UI;
|
import com.vaadin.flow.component.UI;
|
||||||
import com.vaadin.flow.component.button.Button;
|
import com.vaadin.flow.component.button.Button;
|
||||||
|
@ -21,30 +17,27 @@ import com.vaadin.flow.component.dependency.Uses;
|
||||||
import com.vaadin.flow.component.formlayout.FormLayout;
|
import com.vaadin.flow.component.formlayout.FormLayout;
|
||||||
import com.vaadin.flow.component.grid.GridVariant;
|
import com.vaadin.flow.component.grid.GridVariant;
|
||||||
import com.vaadin.flow.component.html.Div;
|
import com.vaadin.flow.component.html.Div;
|
||||||
|
import com.vaadin.flow.component.html.Span;
|
||||||
import com.vaadin.flow.component.icon.Icon;
|
import com.vaadin.flow.component.icon.Icon;
|
||||||
import com.vaadin.flow.component.notification.Notification;
|
import com.vaadin.flow.component.notification.Notification;
|
||||||
import com.vaadin.flow.component.notification.Notification.Position;
|
import com.vaadin.flow.component.notification.Notification.Position;
|
||||||
import com.vaadin.flow.component.notification.NotificationVariant;
|
import com.vaadin.flow.component.notification.NotificationVariant;
|
||||||
import com.vaadin.flow.component.orderedlayout.FlexComponent;
|
|
||||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
|
||||||
import com.vaadin.flow.component.splitlayout.SplitLayout;
|
import com.vaadin.flow.component.splitlayout.SplitLayout;
|
||||||
import com.vaadin.flow.component.textfield.TextArea;
|
import com.vaadin.flow.component.textfield.TextArea;
|
||||||
import com.vaadin.flow.component.textfield.TextField;
|
import com.vaadin.flow.component.textfield.TextField;
|
||||||
import com.vaadin.flow.data.binder.BeanValidationBinder;
|
import com.vaadin.flow.data.binder.BeanValidationBinder;
|
||||||
import com.vaadin.flow.data.binder.ValidationException;
|
import com.vaadin.flow.data.binder.ValidationException;
|
||||||
import com.vaadin.flow.data.converter.StringToBigDecimalConverter;
|
|
||||||
import com.vaadin.flow.data.renderer.ComponentRenderer;
|
import com.vaadin.flow.data.renderer.ComponentRenderer;
|
||||||
import com.vaadin.flow.data.value.ValueChangeMode;
|
|
||||||
import com.vaadin.flow.router.*;
|
import com.vaadin.flow.router.*;
|
||||||
import jakarta.annotation.security.PermitAll;
|
import jakarta.annotation.security.PermitAll;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.orm.ObjectOptimisticLockingFailureException;
|
import org.springframework.orm.ObjectOptimisticLockingFailureException;
|
||||||
import org.vaadin.klaudeta.PaginatedGrid;
|
import org.vaadin.klaudeta.PaginatedGrid;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.util.HashSet;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
@PermitAll
|
@PermitAll
|
||||||
@PageTitle("Expenses")
|
@PageTitle("Expenses")
|
||||||
|
@ -57,41 +50,35 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
|
||||||
private static final String EXPENSE_EDIT_ROUTE_TEMPLATE = "/%s/edit";
|
private static final String EXPENSE_EDIT_ROUTE_TEMPLATE = "/%s/edit";
|
||||||
|
|
||||||
private final PaginatedGrid<Expense, Objects> grid = new PaginatedGrid<>();
|
private final PaginatedGrid<Expense, Objects> grid = new PaginatedGrid<>();
|
||||||
private final TextField nameFilter = new TextField();
|
|
||||||
private final MultiSelectComboBox<Category> categoryFilter = new MultiSelectComboBox<>();
|
|
||||||
private final Button cancel = new Button("Cancel");
|
private final Button cancel = new Button("Cancel");
|
||||||
private final Button save = new Button("Save");
|
private final Button save = new Button("Save");
|
||||||
private final Button delete = new Button("Delete");
|
private final Button delete = new Button("Delete");
|
||||||
private final BeanValidationBinder<Expense> binder;
|
private final BeanValidationBinder<Expense> binder;
|
||||||
|
|
||||||
private Expense expense;
|
private Expense expense;
|
||||||
private final Long userId;
|
|
||||||
|
|
||||||
private final ExpenseService expenseService;
|
private final ExpenseService expenseService;
|
||||||
private final PersonFacade personFacade;
|
|
||||||
private final CategoryService categoryService;
|
private final CategoryService categoryService;
|
||||||
private final ViewsService viewsService;
|
private final PersonService personService;
|
||||||
private final UserService userService;
|
private final EventService eventService;
|
||||||
private TextField name;
|
private TextField name;
|
||||||
private TextField cost;
|
private TextField cost;
|
||||||
private ComboBox<Category> category;
|
private ComboBox<Category> category;
|
||||||
private TextArea description;
|
private TextArea description;
|
||||||
private Checkbox isPeriodic;
|
private Checkbox isPeriodic;
|
||||||
private Checkbox isPaid;
|
private Checkbox isResolved;
|
||||||
private ComboBox<PeriodUnit> periodUnit;
|
private ComboBox<PeriodUnit> periodUnit;
|
||||||
private TextField periodInterval;
|
private TextField periodInterval;
|
||||||
private DatePicker date;
|
private DatePicker date;
|
||||||
private ComboBox<Person> payer;
|
private MultiSelectComboBox<Person> creditors;
|
||||||
private ComboBox<Person> beneficiary;
|
private MultiSelectComboBox<Person> debtors;
|
||||||
|
private ComboBox<Event> event;
|
||||||
@Autowired
|
public ExpensesView(ExpenseService expenseService, CategoryService categoryService, PersonService personService, EventService eventService) {
|
||||||
public ExpensesView(ExpenseService expenseService, CategoryService categoryService, ViewsService viewsService, UserService userService, PersonFacade personFacade) {
|
|
||||||
this.expenseService = expenseService;
|
this.expenseService = expenseService;
|
||||||
this.categoryService = categoryService;
|
this.categoryService = categoryService;
|
||||||
this.viewsService = viewsService;
|
this.personService = personService;
|
||||||
this.userService = userService;
|
this.eventService = eventService;
|
||||||
this.personFacade = personFacade;
|
|
||||||
this.userId = this.userService.getLoggedInUser().getId();
|
|
||||||
addClassNames("expenses-view");
|
addClassNames("expenses-view");
|
||||||
|
|
||||||
// Create UI
|
// Create UI
|
||||||
|
@ -106,40 +93,24 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
|
||||||
grid.addColumn(Expense::getName).setHeader("Name").setSortable(true).setSortProperty("name");
|
grid.addColumn(Expense::getName).setHeader("Name").setSortable(true).setSortProperty("name");
|
||||||
grid.addColumn(Expense::getCost).setHeader("Amount").setSortable(true).setSortProperty("cost");
|
grid.addColumn(Expense::getCost).setHeader("Amount").setSortable(true).setSortProperty("cost");
|
||||||
grid.addColumn(expenseCategory -> expenseCategory.getCategory().getName()).setHeader("Category").setSortable(true).setSortProperty("category");
|
grid.addColumn(expenseCategory -> expenseCategory.getCategory().getName()).setHeader("Category").setSortable(true).setSortProperty("category");
|
||||||
|
grid.addColumn(Expense::getPeriodInterval).setHeader("Period Interval").setSortable(true);
|
||||||
|
grid.addColumn(Expense::getPeriodUnit).setHeader("Period Unit").setSortable(true);
|
||||||
grid.addColumn(Expense::getDate).setHeader("Date").setSortable(true).setSortProperty("date");
|
grid.addColumn(Expense::getDate).setHeader("Date").setSortable(true).setSortProperty("date");
|
||||||
grid.addColumn(new ComponentRenderer<>(this.viewsService::createExpenseBadge)).setHeader("Status").setSortable(true);
|
// grid.addColumn(expenseEvent -> expenseEvent.getEvent().getName()).setHeader("Event").setSortable(true);
|
||||||
|
|
||||||
|
grid.addColumn(new ComponentRenderer<>(expense1 -> createBadge(expenseService.isExpenseResolved(expense1)))).setHeader("Status").setSortable(true);
|
||||||
grid.getColumns().forEach(col -> col.setAutoWidth(true));
|
grid.getColumns().forEach(col -> col.setAutoWidth(true));
|
||||||
|
|
||||||
grid.setItems(this.expenseService.findAllOrderByDateDescending(userId));
|
grid.setItems(this.expenseService.findAllOrderByDateDescending());
|
||||||
grid.setPaginatorSize(5);
|
grid.setPaginatorSize(5);
|
||||||
grid.setPageSize(22); // setting page size
|
grid.setPageSize(25); // setting page size
|
||||||
grid.addThemeVariants(GridVariant.LUMO_NO_BORDER);
|
grid.addThemeVariants(GridVariant.LUMO_NO_BORDER);
|
||||||
|
|
||||||
// Filtering setup - Name
|
|
||||||
nameFilter.setPlaceholder("Filter by Name...");
|
|
||||||
nameFilter.setClearButtonVisible(true);
|
|
||||||
nameFilter.setValueChangeMode(ValueChangeMode.LAZY);
|
|
||||||
nameFilter.addValueChangeListener(e -> this.viewsService.applyNameFilter(nameFilter, userId, grid));
|
|
||||||
|
|
||||||
// Filtering setup - Category
|
|
||||||
categoryFilter.setPlaceholder("Filter by Category...");
|
|
||||||
categoryFilter.setClearButtonVisible(true);
|
|
||||||
categoryFilter.setItems(categoryService.findAllByUserId(userId));
|
|
||||||
categoryFilter.setItemLabelGenerator(Category::getName);
|
|
||||||
categoryFilter.addValueChangeListener(e -> this.viewsService.applyCategoryFilter(categoryFilter, userId, grid));
|
|
||||||
|
|
||||||
// Add filter fields to layout (above the grid)
|
|
||||||
VerticalLayout layout = new VerticalLayout();
|
|
||||||
HorizontalLayout filterLayout = new HorizontalLayout(nameFilter, categoryFilter);
|
|
||||||
filterLayout.setSpacing(true);
|
|
||||||
filterLayout.setAlignItems(FlexComponent.Alignment.BASELINE);
|
|
||||||
layout.add(filterLayout, grid);
|
|
||||||
splitLayout.addToPrimary(layout);
|
|
||||||
|
|
||||||
// when a row is selected or deselected, populate form
|
// when a row is selected or deselected, populate form
|
||||||
grid.asSingleSelect().addValueChangeListener(event -> {
|
grid.asSingleSelect().addValueChangeListener(event -> {
|
||||||
if (event.getValue() != null) UI.getCurrent().navigate(String.format(EXPENSE_EDIT_ROUTE_TEMPLATE, event.getValue().getId()));
|
if (event.getValue() != null) {
|
||||||
else {
|
UI.getCurrent().navigate(String.format(EXPENSE_EDIT_ROUTE_TEMPLATE, event.getValue().getId()));
|
||||||
|
} else {
|
||||||
clearForm();
|
clearForm();
|
||||||
UI.getCurrent().navigate(ExpensesView.class);
|
UI.getCurrent().navigate(ExpensesView.class);
|
||||||
}
|
}
|
||||||
|
@ -147,25 +118,13 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
|
||||||
|
|
||||||
// Configure Form
|
// Configure Form
|
||||||
binder = new BeanValidationBinder<>(Expense.class);
|
binder = new BeanValidationBinder<>(Expense.class);
|
||||||
|
|
||||||
|
// Bind fields. This is where you'd define e.g. validation rules
|
||||||
binder.bindInstanceFields(this);
|
binder.bindInstanceFields(this);
|
||||||
binder.forField(name)
|
|
||||||
.asRequired("Name is required")
|
|
||||||
.bind(Expense::getName, Expense::setName);
|
|
||||||
binder.forField(cost)
|
|
||||||
.asRequired("Cost is required")
|
|
||||||
.withConverter(new StringToBigDecimalConverter("Invalid cost"))
|
|
||||||
.withValidator(costValue -> costValue.compareTo(BigDecimal.ONE) > 0, "Cost must be greater than 1")
|
|
||||||
.bind(Expense::getCost, Expense::setCost);
|
|
||||||
binder.forField(category)
|
|
||||||
.asRequired("Category is required")
|
|
||||||
.bind(Expense::getCategory, Expense::setCategory);
|
|
||||||
binder.forField(date)
|
|
||||||
.asRequired("Date is required")
|
|
||||||
.bind(Expense::getDate, Expense::setDate);
|
|
||||||
|
|
||||||
// We set initial value of isPeriodic to true and show period fields
|
// We set initial value of isPeriodic to true and show period fields
|
||||||
isPeriodic.setValue(false);
|
isPeriodic.setValue(false);
|
||||||
isPaid.setValue(false);
|
isResolved.setValue(false);
|
||||||
periodUnit.setVisible(false);
|
periodUnit.setVisible(false);
|
||||||
periodInterval.setVisible(false);
|
periodInterval.setVisible(false);
|
||||||
|
|
||||||
|
@ -182,6 +141,22 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Event listeners that will remove the selected creditors from the debtors list and vice versa
|
||||||
|
// Done so that the user cant create an expense with the same person as creditor and debtor
|
||||||
|
debtors.addValueChangeListener(event -> {
|
||||||
|
Set<Person> selectedDebtors = event.getValue();
|
||||||
|
final var creditorsSet = new HashSet<>(personService.findAll());
|
||||||
|
creditorsSet.removeIf(selectedDebtors::contains);
|
||||||
|
creditors.setItems(creditorsSet);
|
||||||
|
});
|
||||||
|
|
||||||
|
creditors.addValueChangeListener(event -> {
|
||||||
|
Set<Person> selectedCreditors = event.getValue();
|
||||||
|
final var debtorsSet = new HashSet<>(personService.findAll());
|
||||||
|
debtorsSet.removeIf(selectedCreditors::contains);
|
||||||
|
debtors.setItems(debtorsSet);
|
||||||
|
});
|
||||||
|
|
||||||
cancel.addClickListener(e -> {
|
cancel.addClickListener(e -> {
|
||||||
clearForm();
|
clearForm();
|
||||||
refreshGrid();
|
refreshGrid();
|
||||||
|
@ -189,9 +164,11 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
|
||||||
|
|
||||||
save.addClickListener(e -> {
|
save.addClickListener(e -> {
|
||||||
try {
|
try {
|
||||||
if (this.expense == null) this.expense = new Expense();
|
if (this.expense == null) {
|
||||||
|
this.expense = new Expense();
|
||||||
|
}
|
||||||
binder.writeBean(this.expense);
|
binder.writeBean(this.expense);
|
||||||
expenseService.update(this.expense, userId);
|
expenseService.update(this.expense);
|
||||||
clearForm();
|
clearForm();
|
||||||
refreshGrid();
|
refreshGrid();
|
||||||
Notification.show("Data updated");
|
Notification.show("Data updated");
|
||||||
|
@ -202,13 +179,13 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
|
||||||
n.setPosition(Position.MIDDLE);
|
n.setPosition(Position.MIDDLE);
|
||||||
n.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
n.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||||
} catch (ValidationException validationException) {
|
} catch (ValidationException validationException) {
|
||||||
Notification.show("Failed to update the expense. Check again that all values are valid");
|
Notification.show("Failed to update the data. Check again that all values are valid");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
delete.addClickListener(e -> {
|
delete.addClickListener(e -> {
|
||||||
try {
|
try {
|
||||||
if (Objects.isNull(this.expense)) throw new IllegalStateException("Expense is null!");
|
if (Objects.isNull(this.expense)) throw new RuntimeException("Expense is null!"); //TODO: create proper exception
|
||||||
expenseService.delete(this.expense.getId());
|
expenseService.delete(this.expense.getId());
|
||||||
clearForm();
|
clearForm();
|
||||||
refreshGrid();
|
refreshGrid();
|
||||||
|
@ -216,26 +193,25 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
|
||||||
UI.getCurrent().navigate(ExpensesView.class);
|
UI.getCurrent().navigate(ExpensesView.class);
|
||||||
} catch (ObjectOptimisticLockingFailureException exception) {
|
} catch (ObjectOptimisticLockingFailureException exception) {
|
||||||
Notification n = Notification.show(
|
Notification n = Notification.show(
|
||||||
"Error deleting the data. Somebody else has updated the record while you were making changes.");
|
"Error updating the data. Somebody else has updated the record while you were making changes.");
|
||||||
n.setPosition(Position.MIDDLE);
|
n.setPosition(Position.MIDDLE);
|
||||||
n.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
n.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
initializeComboBoxes();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void beforeEnter(BeforeEnterEvent event) {
|
public void beforeEnter(BeforeEnterEvent event) {
|
||||||
initializeComboBoxes();
|
|
||||||
Optional<Long> expenseId = event.getRouteParameters().get(EXPENSE_ID).map(Long::parseLong);
|
Optional<Long> expenseId = event.getRouteParameters().get(EXPENSE_ID).map(Long::parseLong);
|
||||||
if (expenseId.isPresent()) {
|
if (expenseId.isPresent()) {
|
||||||
Optional<Expense> expenseFromBackend = expenseService.get(expenseId.get());
|
Optional<Expense> expenseFromBackend = expenseService.get(expenseId.get());
|
||||||
if (expenseFromBackend.isPresent()) populateForm(expenseFromBackend.get());
|
if (expenseFromBackend.isPresent()) populateForm(expenseFromBackend.get());
|
||||||
else {
|
else {
|
||||||
Notification.show(
|
Notification.show(
|
||||||
String.format("The requested expense was not found, ID = %s", expenseId.get()), 3000,
|
String.format("The requested expense was not found, ID = %s", expenseId.get()), 3000,
|
||||||
Notification.Position.BOTTOM_START);
|
Notification.Position.BOTTOM_START);
|
||||||
|
// when a row is selected but the data is no longer available,
|
||||||
|
// refresh grid
|
||||||
refreshGrid();
|
refreshGrid();
|
||||||
event.forwardTo(ExpensesView.class);
|
event.forwardTo(ExpensesView.class);
|
||||||
}
|
}
|
||||||
|
@ -245,36 +221,39 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
|
||||||
private void createEditorLayout(SplitLayout splitLayout) {
|
private void createEditorLayout(SplitLayout splitLayout) {
|
||||||
Div editorLayoutDiv = new Div();
|
Div editorLayoutDiv = new Div();
|
||||||
editorLayoutDiv.setClassName("editor-layout");
|
editorLayoutDiv.setClassName("editor-layout");
|
||||||
|
|
||||||
Div editorDiv = new Div();
|
Div editorDiv = new Div();
|
||||||
editorDiv.setClassName("editor");
|
editorDiv.setClassName("editor");
|
||||||
editorLayoutDiv.add(editorDiv);
|
editorLayoutDiv.add(editorDiv);
|
||||||
final var people = this.personFacade.findAllByUserId(userId);
|
|
||||||
|
|
||||||
FormLayout formLayout = new FormLayout();
|
FormLayout formLayout = new FormLayout();
|
||||||
name = new TextField("Name");
|
name = new TextField("Name");
|
||||||
cost = new TextField("Cost");
|
cost = new TextField("Cost");
|
||||||
category = new ComboBox<>("Category");
|
category = new ComboBox<>("Category");
|
||||||
category.setItems(categoryService.findAllByUserId(userId));
|
category.setItems(categoryService.findAll());
|
||||||
category.setItemLabelGenerator(Category::getName);
|
category.setItemLabelGenerator(Category::getName);
|
||||||
description = new TextArea("Description");
|
description = new TextArea("Description");
|
||||||
periodUnit = new ComboBox<>("Period Unit");
|
periodUnit = new ComboBox<>("Period Unit");
|
||||||
periodUnit.setItems(PeriodUnit.values());
|
periodUnit.setItems(PeriodUnit.values());
|
||||||
periodInterval = new TextField("Period Interval");
|
periodInterval = new TextField("Period Interval");
|
||||||
payer = new ComboBox<>("Payer");
|
creditors = new MultiSelectComboBox<>("Creditors");
|
||||||
payer.setItems(people);
|
creditors.setItems(personService.findAll());
|
||||||
payer.setItemLabelGenerator(person -> person.getFirstName() + " " + person.getLastName());
|
creditors.setItemLabelGenerator(Person::getFirstName);
|
||||||
beneficiary = new ComboBox<>("Beneficiary");
|
event = new ComboBox<>("Event");
|
||||||
beneficiary.setItems(people);
|
event.setItems(eventService.findAll());
|
||||||
beneficiary.setItemLabelGenerator(person -> person.getFirstName() + " " + person.getLastName());
|
event.setItemLabelGenerator(Event::getName);
|
||||||
|
debtors = new MultiSelectComboBox<>("Debtors");
|
||||||
|
debtors.setItems(personService.findAll());
|
||||||
|
debtors.setItemLabelGenerator(Person::getFirstName);
|
||||||
date = new DatePicker("Date");
|
date = new DatePicker("Date");
|
||||||
|
|
||||||
// Horizontal layout for checkboxes
|
// Horizontal layout for checkboxes
|
||||||
HorizontalLayout checkboxLayout = new HorizontalLayout();
|
HorizontalLayout checkboxLayout = new HorizontalLayout();
|
||||||
isPeriodic = new Checkbox("Is Periodic");
|
isPeriodic = new Checkbox("Is Periodic");
|
||||||
isPaid = new Checkbox("Paid");
|
isResolved = new Checkbox("Paid");
|
||||||
checkboxLayout.add(isPeriodic, isPaid);
|
checkboxLayout.add(isPeriodic, isResolved);
|
||||||
|
|
||||||
formLayout.add(name, cost, category, description, checkboxLayout, periodUnit, periodInterval, date, payer, beneficiary);
|
formLayout.add(name, cost, category, description, checkboxLayout, periodUnit, periodInterval, date, creditors, debtors, event);
|
||||||
editorDiv.add(formLayout);
|
editorDiv.add(formLayout);
|
||||||
createButtonLayout(editorLayoutDiv);
|
createButtonLayout(editorLayoutDiv);
|
||||||
|
|
||||||
|
@ -299,7 +278,7 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void refreshGrid() {
|
private void refreshGrid() {
|
||||||
grid.setItems(this.expenseService.findAllOrderByDateDescending(userId));
|
grid.setItems(this.expenseService.findAllOrderByDateDescending());
|
||||||
grid.select(null);
|
grid.select(null);
|
||||||
grid.getDataProvider().refreshAll();
|
grid.getDataProvider().refreshAll();
|
||||||
}
|
}
|
||||||
|
@ -316,9 +295,15 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
|
||||||
periodInterval.setVisible(isPeriodicChecked);
|
periodInterval.setVisible(isPeriodicChecked);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeComboBoxes() {
|
private Span createBadge(Boolean isExpenseResolved) {
|
||||||
final var loggedInPerson = this.personFacade.getLoggedInPerson();
|
Span badge = new Span();
|
||||||
payer.setValue(loggedInPerson);
|
if (Boolean.TRUE.equals(isExpenseResolved)) {
|
||||||
beneficiary.setValue(loggedInPerson);
|
badge.setText("Resolved");
|
||||||
|
badge.getElement().getThemeList().add("badge success");
|
||||||
|
} else {
|
||||||
|
badge.setText("To be Resolved");
|
||||||
|
badge.getElement().getThemeList().add("badge error");
|
||||||
|
}
|
||||||
|
return badge;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,9 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver {
|
||||||
login.setI18n(i18n);
|
login.setI18n(i18n);
|
||||||
|
|
||||||
// Add a listener for the Forgot password button
|
// Add a listener for the Forgot password button
|
||||||
login.addForgotPasswordListener(event -> Notification.show("Tough shit, feature aint ready yet!", 3000, Notification.Position.BOTTOM_CENTER));
|
login.addForgotPasswordListener(event -> {
|
||||||
|
Notification.show("Tough shit, feature aint ready yet!", 3000, Notification.Position.BOTTOM_CENTER);
|
||||||
|
});
|
||||||
|
|
||||||
add(new H1("Munera"), new H2("An expense tracking application"), login);
|
add(new H1("Munera"), new H2("An expense tracking application"), login);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,8 @@ package com.application.munera.views.people;
|
||||||
|
|
||||||
import com.application.munera.data.Expense;
|
import com.application.munera.data.Expense;
|
||||||
import com.application.munera.data.Person;
|
import com.application.munera.data.Person;
|
||||||
import com.application.munera.data.User;
|
import com.application.munera.services.ExpenseService;
|
||||||
import com.application.munera.facades.ExpenseFacade;
|
import com.application.munera.services.PersonService;
|
||||||
import com.application.munera.facades.PersonFacade;
|
|
||||||
import com.application.munera.services.UserService;
|
|
||||||
import com.application.munera.services.ViewsService;
|
|
||||||
import com.application.munera.views.MainLayout;
|
import com.application.munera.views.MainLayout;
|
||||||
import com.vaadin.flow.component.UI;
|
import com.vaadin.flow.component.UI;
|
||||||
import com.vaadin.flow.component.button.Button;
|
import com.vaadin.flow.component.button.Button;
|
||||||
|
@ -33,9 +30,9 @@ import com.vaadin.flow.router.BeforeEnterObserver;
|
||||||
import com.vaadin.flow.router.PageTitle;
|
import com.vaadin.flow.router.PageTitle;
|
||||||
import com.vaadin.flow.router.Route;
|
import com.vaadin.flow.router.Route;
|
||||||
import jakarta.annotation.security.PermitAll;
|
import jakarta.annotation.security.PermitAll;
|
||||||
import org.springframework.dao.DataIntegrityViolationException;
|
|
||||||
import org.springframework.orm.ObjectOptimisticLockingFailureException;
|
import org.springframework.orm.ObjectOptimisticLockingFailureException;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@ -57,23 +54,15 @@ public class PeopleView extends Div implements BeforeEnterObserver {
|
||||||
private final BeanValidationBinder<Person> binder;
|
private final BeanValidationBinder<Person> binder;
|
||||||
|
|
||||||
private Person person;
|
private Person person;
|
||||||
private final User loggedUser;
|
private final PersonService personService;
|
||||||
private final Long userId;
|
private final ExpenseService expenseService;
|
||||||
private final UserService userService;
|
|
||||||
private final PersonFacade personFacade;
|
|
||||||
private final ExpenseFacade expenseFacade;
|
|
||||||
private final ViewsService viewsService;
|
|
||||||
private TextField firstName;
|
private TextField firstName;
|
||||||
private TextField lastName;
|
private TextField lastName;
|
||||||
private EmailField email;
|
private EmailField email;
|
||||||
|
|
||||||
public PeopleView(ViewsService viewsService, PersonFacade personFacade, ExpenseFacade expenseFacade, UserService userService) {
|
public PeopleView(PersonService personService, ExpenseService expenseService) {
|
||||||
this.viewsService = viewsService;
|
this.personService = personService;
|
||||||
this.personFacade = personFacade;
|
this.expenseService = expenseService;
|
||||||
this.expenseFacade = expenseFacade;
|
|
||||||
this.userService = userService;
|
|
||||||
loggedUser = userService.getLoggedInUser();
|
|
||||||
userId = loggedUser.getId();
|
|
||||||
addClassNames("expenses-view");
|
addClassNames("expenses-view");
|
||||||
|
|
||||||
// Create UI
|
// Create UI
|
||||||
|
@ -86,40 +75,13 @@ public class PeopleView extends Div implements BeforeEnterObserver {
|
||||||
|
|
||||||
grid.addThemeVariants(GridVariant.LUMO_NO_BORDER);
|
grid.addThemeVariants(GridVariant.LUMO_NO_BORDER);
|
||||||
grid.addHierarchyColumn(this::getNodeName).setHeader("Name");
|
grid.addHierarchyColumn(this::getNodeName).setHeader("Name");
|
||||||
grid.addColumn(this::getNodeCost).setHeader("Balance").setSortable(true);
|
grid.addColumn(this::getNodeCost).setHeader("Total Expenses Value").setSortable(true);
|
||||||
grid.addColumn(new ComponentRenderer<>(personEntry -> {
|
grid.addColumn(new ComponentRenderer<>(persona -> {
|
||||||
if (personEntry instanceof Person person1) return this.viewsService.createPersonBadge(personFacade.calculateNetBalance(person1));
|
if (persona instanceof Person) return createPersonBadge(personService.calculateNetBalance((Person) persona));
|
||||||
else return this.viewsService.createExpenseBadge(((Expense) personEntry));
|
else return createExpenseBadge(((Expense) persona).getIsResolved());
|
||||||
})).setHeader("Balance Status");
|
})).setHeader("Balance Status");
|
||||||
|
|
||||||
grid.addColumn(new ComponentRenderer<>(persona -> {
|
List<Person> people = (List<Person>) personService.findAll();
|
||||||
switch (persona) {
|
|
||||||
case Person person1 -> {
|
|
||||||
Button setDebtPaidButton = new Button("Set all debt as paid", event -> this.personFacade.setDebtPaid(person1, grid, userId));
|
|
||||||
setDebtPaidButton.addThemeVariants(ButtonVariant.LUMO_SMALL, ButtonVariant.LUMO_PRIMARY);
|
|
||||||
return setDebtPaidButton;
|
|
||||||
}
|
|
||||||
case Expense expense -> {
|
|
||||||
Button setExpensePaidButton = new Button("Set as paid", event -> this.expenseFacade.setExpensePaid(expense, grid, userId));
|
|
||||||
setExpensePaidButton.addThemeVariants(ButtonVariant.LUMO_SMALL);
|
|
||||||
if (Boolean.TRUE.equals((expense).getIsPaid())) setExpensePaidButton.setEnabled(false);
|
|
||||||
return setExpensePaidButton;
|
|
||||||
}
|
|
||||||
default -> {
|
|
||||||
return new Span();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
grid.addColumn(new ComponentRenderer<>(persona -> {
|
|
||||||
if (persona instanceof Person person1) {
|
|
||||||
Button setCreditPaidButton = new Button("Set all credit as paid", event -> this.personFacade.setCreditPaid(person1, grid, userId));
|
|
||||||
setCreditPaidButton.addThemeVariants(ButtonVariant.LUMO_SMALL, ButtonVariant.LUMO_PRIMARY);
|
|
||||||
return setCreditPaidButton;
|
|
||||||
} else return new Span();
|
|
||||||
}));
|
|
||||||
|
|
||||||
List<Person> people = personFacade.findAllExcludeLoggedUser(loggedUser);
|
|
||||||
|
|
||||||
this.setGridData(people);
|
this.setGridData(people);
|
||||||
|
|
||||||
|
@ -133,14 +95,8 @@ public class PeopleView extends Div implements BeforeEnterObserver {
|
||||||
binder = new BeanValidationBinder<>(Person.class);
|
binder = new BeanValidationBinder<>(Person.class);
|
||||||
|
|
||||||
// Bind fields. This is where you'd define e.g. validation rules
|
// Bind fields. This is where you'd define e.g. validation rules
|
||||||
binder.bindInstanceFields(this);
|
|
||||||
binder.forField(firstName)
|
|
||||||
.asRequired("First Name is required")
|
|
||||||
.bind(Person::getFirstName, Person::setFirstName);
|
|
||||||
|
|
||||||
binder.forField(lastName)
|
binder.bindInstanceFields(this);
|
||||||
.asRequired("Last Name is required")
|
|
||||||
.bind(Person::getLastName, Person::setLastName);
|
|
||||||
|
|
||||||
cancel.addClickListener(e -> {
|
cancel.addClickListener(e -> {
|
||||||
clearForm();
|
clearForm();
|
||||||
|
@ -149,9 +105,11 @@ public class PeopleView extends Div implements BeforeEnterObserver {
|
||||||
|
|
||||||
save.addClickListener(e -> {
|
save.addClickListener(e -> {
|
||||||
try {
|
try {
|
||||||
if (this.person == null) this.person = new Person();
|
if (this.person == null) {
|
||||||
|
this.person = new Person();
|
||||||
|
}
|
||||||
binder.writeBean(this.person);
|
binder.writeBean(this.person);
|
||||||
personFacade.update(this.person, userId);
|
personService.update(this.person);
|
||||||
clearForm();
|
clearForm();
|
||||||
refreshGrid();
|
refreshGrid();
|
||||||
Notification.show("Data updated");
|
Notification.show("Data updated");
|
||||||
|
@ -162,14 +120,14 @@ public class PeopleView extends Div implements BeforeEnterObserver {
|
||||||
n.setPosition(Position.MIDDLE);
|
n.setPosition(Position.MIDDLE);
|
||||||
n.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
n.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||||
} catch (ValidationException validationException) {
|
} catch (ValidationException validationException) {
|
||||||
Notification.show("Failed to update the person. Check again that all values are valid");
|
Notification.show("Failed to update the data. Check again that all values are valid");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
delete.addClickListener(e -> {
|
delete.addClickListener(e -> {
|
||||||
try {
|
try {
|
||||||
if (this.person == null) throw new IllegalStateException("The person is null!");
|
if (this.person == null) throw new RuntimeException("The person is null!"); //TODO: create proper exception
|
||||||
personFacade.delete(this.person.getId());
|
personService.delete(this.person.getId());
|
||||||
clearForm();
|
clearForm();
|
||||||
refreshGrid();
|
refreshGrid();
|
||||||
Notification.show("Data delete");
|
Notification.show("Data delete");
|
||||||
|
@ -179,24 +137,19 @@ public class PeopleView extends Div implements BeforeEnterObserver {
|
||||||
"Error updating the data. Somebody else has updated the record while you were making changes.");
|
"Error updating the data. Somebody else has updated the record while you were making changes.");
|
||||||
n.setPosition(Position.MIDDLE);
|
n.setPosition(Position.MIDDLE);
|
||||||
n.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
n.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||||
} catch (DataIntegrityViolationException ex) {
|
|
||||||
Notification n = Notification.show(
|
|
||||||
"Cannot delete this person as it is associated with existing expenses.");
|
|
||||||
n.setPosition(Notification.Position.MIDDLE);
|
|
||||||
n.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getNodeName(Object node) {
|
private String getNodeName(Object node) {
|
||||||
if (node instanceof Person person1) return (person1).getFirstName() + " " + (person1).getLastName();
|
if (node instanceof Person) return ((Person) node).getFirstName() + " " + ((Person) node).getLastName();
|
||||||
else if (node instanceof Expense expense1) return (expense1).getName();
|
else if (node instanceof Expense) return ((Expense) node).getName();
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getNodeCost(Object node) {
|
private String getNodeCost(Object node) {
|
||||||
if (node instanceof Person person1) return this.personFacade.calculateNetBalance(person1) + " €";
|
if (node instanceof Person) return this.personService.calculateNetBalance((Person) node).toString() + " €";
|
||||||
else if (node instanceof Expense expense1) return (expense1).getCost().toString() + " €";
|
else if (node instanceof Expense) return ((Expense) node).getCost().toString() + " €";
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,13 +157,15 @@ public class PeopleView extends Div implements BeforeEnterObserver {
|
||||||
public void beforeEnter(BeforeEnterEvent event) {
|
public void beforeEnter(BeforeEnterEvent event) {
|
||||||
Optional<Long> personId = event.getRouteParameters().get(PERSON_ID).map(Long::parseLong);
|
Optional<Long> personId = event.getRouteParameters().get(PERSON_ID).map(Long::parseLong);
|
||||||
if (personId.isPresent()) {
|
if (personId.isPresent()) {
|
||||||
Optional<Person> personFromBackend = personFacade.findById(personId.get());
|
Optional<Person> personFromBackend = personService.get(personId.get());
|
||||||
if (personFromBackend.isPresent()) populateForm(personFromBackend.get());
|
if (personFromBackend.isPresent()) populateForm(personFromBackend.get());
|
||||||
else {
|
else {
|
||||||
Notification.show(
|
Notification.show(
|
||||||
String.format("The requested person was not found, ID = %s", personId.get()), 3000,
|
String.format("The requested person was not found, ID = %s", personId.get()), 3000,
|
||||||
Position.BOTTOM_START);
|
Position.BOTTOM_START);
|
||||||
refreshGrid(); // when a row is selected but the data is no longer available refresh grid
|
// when a row is selected but the data is no longer available,
|
||||||
|
// refresh grid
|
||||||
|
refreshGrid();
|
||||||
event.forwardTo(PeopleView.class);
|
event.forwardTo(PeopleView.class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -219,6 +174,7 @@ public class PeopleView extends Div implements BeforeEnterObserver {
|
||||||
private void createEditorLayout(SplitLayout splitLayout) {
|
private void createEditorLayout(SplitLayout splitLayout) {
|
||||||
Div editorLayoutDiv = new Div();
|
Div editorLayoutDiv = new Div();
|
||||||
editorLayoutDiv.setClassName("editor-layout");
|
editorLayoutDiv.setClassName("editor-layout");
|
||||||
|
|
||||||
Div editorDiv = new Div();
|
Div editorDiv = new Div();
|
||||||
editorDiv.setClassName("editor");
|
editorDiv.setClassName("editor");
|
||||||
editorLayoutDiv.add(editorDiv);
|
editorLayoutDiv.add(editorDiv);
|
||||||
|
@ -270,13 +226,40 @@ public class PeopleView extends Div implements BeforeEnterObserver {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Span createPersonBadge(BigDecimal netBalance) {
|
||||||
|
Span badge = new Span();
|
||||||
|
if (netBalance.compareTo(BigDecimal.ZERO) < 0) {
|
||||||
|
badge.setText("Credit");
|
||||||
|
badge.getElement().getThemeList().add("badge success");
|
||||||
|
} else if (netBalance.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
badge.setText("Debit");
|
||||||
|
badge.getElement().getThemeList().add("badge error");
|
||||||
|
} else {
|
||||||
|
badge.setText("Clear");
|
||||||
|
badge.getElement().getThemeList().add("badge contrast");
|
||||||
|
}
|
||||||
|
return badge;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Span createExpenseBadge(Boolean isExpenseResolved) {
|
||||||
|
Span badge = new Span();
|
||||||
|
if (Boolean.TRUE.equals(isExpenseResolved)) {
|
||||||
|
badge.setText("Resolved");
|
||||||
|
badge.getElement().getThemeList().add("badge success");
|
||||||
|
} else {
|
||||||
|
badge.setText("To be Resolved");
|
||||||
|
badge.getElement().getThemeList().add("badge error");
|
||||||
|
}
|
||||||
|
return badge;
|
||||||
|
}
|
||||||
|
|
||||||
public void setGridData(List<Person> people) {
|
public void setGridData(List<Person> people) {
|
||||||
for (Person person : people) {
|
for (Person person : people) {
|
||||||
// Add the person as a root item
|
// Add the person as a root item
|
||||||
grid.getTreeData().addItem(null, person);
|
grid.getTreeData().addItem(null, person);
|
||||||
|
|
||||||
// Fetch expenses for the current person
|
// Fetch expenses for the current person
|
||||||
List<Expense> expenses = expenseFacade.findExpensesByPerson(person);
|
List<Expense> expenses = expenseService.findExpenseByUser(person);
|
||||||
|
|
||||||
// Add each expense as a child item under the person
|
// Add each expense as a child item under the person
|
||||||
for (Expense expense : expenses) grid.getTreeData().addItem(person, expense);
|
for (Expense expense : expenses) grid.getTreeData().addItem(person, expense);
|
||||||
|
|
|
@ -1,119 +0,0 @@
|
||||||
package com.application.munera.views.settings;
|
|
||||||
|
|
||||||
import com.application.munera.data.User;
|
|
||||||
import com.application.munera.services.UserService;
|
|
||||||
import com.application.munera.views.MainLayout;
|
|
||||||
import com.vaadin.flow.component.button.Button;
|
|
||||||
import com.vaadin.flow.component.dependency.Uses;
|
|
||||||
import com.vaadin.flow.component.formlayout.FormLayout;
|
|
||||||
import com.vaadin.flow.component.icon.Icon;
|
|
||||||
import com.vaadin.flow.component.notification.Notification;
|
|
||||||
import com.vaadin.flow.component.notification.NotificationVariant;
|
|
||||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
|
||||||
import com.vaadin.flow.component.textfield.EmailField;
|
|
||||||
import com.vaadin.flow.component.textfield.PasswordField;
|
|
||||||
import com.vaadin.flow.component.textfield.TextField;
|
|
||||||
import com.vaadin.flow.data.binder.BeanValidationBinder;
|
|
||||||
import com.vaadin.flow.data.binder.ValidationException;
|
|
||||||
import com.vaadin.flow.router.BeforeEnterEvent;
|
|
||||||
import com.vaadin.flow.router.BeforeEnterObserver;
|
|
||||||
import com.vaadin.flow.router.PageTitle;
|
|
||||||
import com.vaadin.flow.router.Route;
|
|
||||||
import jakarta.annotation.security.PermitAll;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.orm.ObjectOptimisticLockingFailureException;
|
|
||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
|
||||||
|
|
||||||
@PageTitle("Settings")
|
|
||||||
@PermitAll
|
|
||||||
@Uses(Icon.class)
|
|
||||||
@Route(value = "settings", layout = MainLayout.class)
|
|
||||||
public class SettingsView extends VerticalLayout implements BeforeEnterObserver {
|
|
||||||
|
|
||||||
private final UserService userService;
|
|
||||||
|
|
||||||
private TextField firstName;
|
|
||||||
private TextField lastName;
|
|
||||||
private PasswordField password;
|
|
||||||
private TextField monthlyIncome;
|
|
||||||
private EmailField email;
|
|
||||||
private final BeanValidationBinder<User> binder;
|
|
||||||
private final Button save = new Button("Save");
|
|
||||||
private final User loggedInUser;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public SettingsView(UserService userService) {
|
|
||||||
this.userService = userService;
|
|
||||||
|
|
||||||
createForm();
|
|
||||||
|
|
||||||
loggedInUser = userService.getLoggedInUser();
|
|
||||||
|
|
||||||
binder = new BeanValidationBinder<>(User.class);
|
|
||||||
// Bind fields. This is where you'd define e.g. validation rules
|
|
||||||
binder.bindInstanceFields(this);
|
|
||||||
binder.forField(firstName)
|
|
||||||
.asRequired("First name is required")
|
|
||||||
.bind(User::getFirstName, User::setFirstName);
|
|
||||||
|
|
||||||
binder.forField(lastName)
|
|
||||||
.asRequired("Last name is required")
|
|
||||||
.bind(User::getLastName, User::setLastName);
|
|
||||||
|
|
||||||
binder.forField(password)
|
|
||||||
.asRequired("Password is required")
|
|
||||||
.bind(User::getPassword, User::setPassword);
|
|
||||||
|
|
||||||
save.addClickListener(e -> {
|
|
||||||
try {
|
|
||||||
binder.writeBean(this.loggedInUser);
|
|
||||||
this.saveUserData();
|
|
||||||
Notification.show("User details updated successfully");
|
|
||||||
} catch (ObjectOptimisticLockingFailureException exception) {
|
|
||||||
Notification n = Notification.show(
|
|
||||||
"Error updating the data. Somebody else has updated the record while you were making changes.");
|
|
||||||
n.setPosition(Notification.Position.MIDDLE);
|
|
||||||
n.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
|
||||||
} catch (ValidationException validationException) {
|
|
||||||
Notification.show("Failed to update the user. Check again that all values are valid");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createForm() {
|
|
||||||
FormLayout formLayout = new FormLayout();
|
|
||||||
|
|
||||||
firstName = new TextField("First Name");
|
|
||||||
lastName = new TextField("Last Name");
|
|
||||||
password = new PasswordField("Password");
|
|
||||||
email = new EmailField("Email");
|
|
||||||
monthlyIncome = new TextField("Monthly Income");
|
|
||||||
|
|
||||||
formLayout.add(firstName, lastName, password, email, monthlyIncome);
|
|
||||||
|
|
||||||
add(formLayout, this.save);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void saveUserData() {
|
|
||||||
|
|
||||||
loggedInUser.setFirstName(firstName.getValue());
|
|
||||||
loggedInUser.setLastName(lastName.getValue());
|
|
||||||
loggedInUser.setEmail(email.getValue());
|
|
||||||
loggedInUser.setPassword(password.getValue());
|
|
||||||
|
|
||||||
// TODO: implement
|
|
||||||
String monthlyIncome = this.monthlyIncome.getValue();
|
|
||||||
|
|
||||||
userService.saveOrUpdateUserAndConnectedPerson(loggedInUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void beforeEnter(BeforeEnterEvent event) {
|
|
||||||
final var getLoggedInUser = userService.getLoggedInUser();
|
|
||||||
firstName.setValue(getLoggedInUser.getFirstName());
|
|
||||||
lastName.setValue(getLoggedInUser.getLastName());
|
|
||||||
password.setValue(getLoggedInUser.getPassword());
|
|
||||||
email.setValue(getLoggedInUser.getEmail());
|
|
||||||
monthlyIncome.setValue("");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,238 +0,0 @@
|
||||||
package com.application.munera.views.users;
|
|
||||||
|
|
||||||
import com.application.munera.data.enums.Role;
|
|
||||||
import com.application.munera.data.User;
|
|
||||||
import com.application.munera.services.UserService;
|
|
||||||
import com.application.munera.views.MainLayout;
|
|
||||||
import com.vaadin.flow.component.UI;
|
|
||||||
import com.vaadin.flow.component.button.Button;
|
|
||||||
import com.vaadin.flow.component.button.ButtonVariant;
|
|
||||||
import com.vaadin.flow.component.combobox.MultiSelectComboBox;
|
|
||||||
import com.vaadin.flow.component.dependency.Uses;
|
|
||||||
import com.vaadin.flow.component.formlayout.FormLayout;
|
|
||||||
import com.vaadin.flow.component.grid.Grid;
|
|
||||||
import com.vaadin.flow.component.grid.GridVariant;
|
|
||||||
import com.vaadin.flow.component.html.Div;
|
|
||||||
import com.vaadin.flow.component.icon.Icon;
|
|
||||||
import com.vaadin.flow.component.notification.Notification;
|
|
||||||
import com.vaadin.flow.component.notification.NotificationVariant;
|
|
||||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
|
||||||
import com.vaadin.flow.component.splitlayout.SplitLayout;
|
|
||||||
import com.vaadin.flow.component.textfield.EmailField;
|
|
||||||
import com.vaadin.flow.component.textfield.PasswordField;
|
|
||||||
import com.vaadin.flow.component.textfield.TextField;
|
|
||||||
import com.vaadin.flow.data.binder.BeanValidationBinder;
|
|
||||||
import com.vaadin.flow.data.binder.ValidationException;
|
|
||||||
import com.vaadin.flow.data.validator.EmailValidator;
|
|
||||||
import com.vaadin.flow.router.BeforeEnterEvent;
|
|
||||||
import com.vaadin.flow.router.BeforeEnterObserver;
|
|
||||||
import com.vaadin.flow.router.PageTitle;
|
|
||||||
import com.vaadin.flow.router.Route;
|
|
||||||
import jakarta.annotation.security.RolesAllowed;
|
|
||||||
import org.springframework.orm.ObjectOptimisticLockingFailureException;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
|
|
||||||
@PageTitle("Users")
|
|
||||||
@RolesAllowed("ADMIN")
|
|
||||||
@Route(value = "users/:userID?/:action?(edit)", layout = MainLayout.class)
|
|
||||||
@Uses(Icon.class)
|
|
||||||
public class UsersView extends Div implements BeforeEnterObserver {
|
|
||||||
|
|
||||||
private static final String USER_ID = "userID";
|
|
||||||
private static final String USER_EDIT_ROUTE_TEMPLATE = "users/%s/edit";
|
|
||||||
|
|
||||||
private final Grid<User> grid = new Grid<>();
|
|
||||||
|
|
||||||
private final Button cancel = new Button("Cancel");
|
|
||||||
private final Button save = new Button("Save");
|
|
||||||
private final Button delete = new Button("Delete");
|
|
||||||
|
|
||||||
private final BeanValidationBinder<User> binder;
|
|
||||||
|
|
||||||
private User user;
|
|
||||||
private final UserService userService;
|
|
||||||
private TextField firstName;
|
|
||||||
private TextField lastName;
|
|
||||||
private TextField username;
|
|
||||||
private PasswordField password;
|
|
||||||
private EmailField email;
|
|
||||||
private MultiSelectComboBox<Role> roles; // Updated to MultiSelectComboBox
|
|
||||||
|
|
||||||
public UsersView(UserService userService) {
|
|
||||||
this.userService = userService;
|
|
||||||
addClassNames("expenses-view");
|
|
||||||
|
|
||||||
// Create UI
|
|
||||||
SplitLayout splitLayout = new SplitLayout();
|
|
||||||
|
|
||||||
createGridLayout(splitLayout);
|
|
||||||
createEditorLayout(splitLayout);
|
|
||||||
|
|
||||||
add(splitLayout);
|
|
||||||
|
|
||||||
grid.addColumn(User::getFirstName).setHeader("First name").setSortable(true);
|
|
||||||
grid.addColumn(User::getLastName).setHeader("Last name").setSortable(true);
|
|
||||||
grid.addColumn(User::getUsername).setHeader("Username").setSortable(true);
|
|
||||||
grid.addColumn(User::getEmail).setHeader("Email").setSortable(true);
|
|
||||||
grid.addColumn(User::getRoles).setHeader("Roles").setSortable(true);
|
|
||||||
grid.getColumns().forEach(col -> col.setAutoWidth(true));
|
|
||||||
|
|
||||||
grid.setItems(this.userService.findAll());
|
|
||||||
grid.addThemeVariants(GridVariant.LUMO_NO_BORDER);
|
|
||||||
|
|
||||||
// when a row is selected or deselected, populate form
|
|
||||||
grid.asSingleSelect().addValueChangeListener(event -> {
|
|
||||||
if (event.getValue() != null) UI.getCurrent().navigate(String.format(USER_EDIT_ROUTE_TEMPLATE, event.getValue().getId()));
|
|
||||||
else {
|
|
||||||
clearForm();
|
|
||||||
UI.getCurrent().navigate(UsersView.class);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
binder = new BeanValidationBinder<>(User.class);
|
|
||||||
// Bind fields with validation rules
|
|
||||||
binder.forField(firstName)
|
|
||||||
.asRequired("First name is required")
|
|
||||||
.bind(User::getFirstName, User::setFirstName);
|
|
||||||
|
|
||||||
binder.forField(lastName)
|
|
||||||
.asRequired("Last name is required")
|
|
||||||
.bind(User::getLastName, User::setLastName);
|
|
||||||
|
|
||||||
binder.forField(username)
|
|
||||||
.asRequired("Username is required")
|
|
||||||
.bind(User::getUsername, User::setUsername);
|
|
||||||
|
|
||||||
binder.forField(password)
|
|
||||||
.asRequired("Password is required")
|
|
||||||
.bind(User::getPassword, User::setPassword);
|
|
||||||
|
|
||||||
binder.forField(email)
|
|
||||||
.withValidator(
|
|
||||||
new EmailValidator("Please enter a valid email address"))
|
|
||||||
.bind(User::getEmail, User::setEmail);
|
|
||||||
|
|
||||||
binder.forField(roles)
|
|
||||||
.bind(user1 -> new HashSet<>(user.getRoleList()), // Getter to convert roles string to list
|
|
||||||
(user1, roles) -> user.setRoleList(new ArrayList<>(roles))); // Setter to convert list back to string
|
|
||||||
|
|
||||||
|
|
||||||
// Button listeners
|
|
||||||
cancel.addClickListener(e -> {
|
|
||||||
clearForm();
|
|
||||||
refreshGrid();
|
|
||||||
});
|
|
||||||
save.addClickListener(e -> {
|
|
||||||
try {
|
|
||||||
if (this.user == null) this.user = new User();
|
|
||||||
binder.writeBean(this.user);
|
|
||||||
this.userService.saveOrUpdateUserAndConnectedPerson(this.user);
|
|
||||||
clearForm();
|
|
||||||
refreshGrid();
|
|
||||||
Notification.show("Data updated");
|
|
||||||
UI.getCurrent().navigate(UsersView.class);
|
|
||||||
} catch (ObjectOptimisticLockingFailureException exception) {
|
|
||||||
Notification n = Notification.show(
|
|
||||||
"Error updating the data. Somebody else has updated the record while you were making changes.");
|
|
||||||
n.setPosition(Notification.Position.MIDDLE);
|
|
||||||
n.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
|
||||||
} catch (ValidationException validationException) {
|
|
||||||
Notification.show("Failed to update the user. Check again that all values are valid");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
delete.addClickListener(e -> {
|
|
||||||
try {
|
|
||||||
if (this.user == null) throw new IllegalStateException("The user is null!");
|
|
||||||
userService.delete(this.user);
|
|
||||||
clearForm();
|
|
||||||
refreshGrid();
|
|
||||||
Notification.show("Data delete");
|
|
||||||
UI.getCurrent().navigate(UsersView.class);
|
|
||||||
} catch (ObjectOptimisticLockingFailureException exception) {
|
|
||||||
Notification n = Notification.show(
|
|
||||||
"Error updating the data. Somebody else has updated the record while you were making changes.");
|
|
||||||
n.setPosition(Notification.Position.MIDDLE);
|
|
||||||
n.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void beforeEnter(BeforeEnterEvent event) {
|
|
||||||
Optional<Long> userId = event.getRouteParameters().get(USER_ID).map(Long::parseLong);
|
|
||||||
if (userId.isPresent()) {
|
|
||||||
Optional<User> userFromBackend = this.userService.findById(userId.get());
|
|
||||||
if (userFromBackend.isPresent()) populateForm(userFromBackend.get());
|
|
||||||
else {
|
|
||||||
Notification.show(
|
|
||||||
String.format("The requested user was not found, ID = %s", userId.get()), 3000,
|
|
||||||
Notification.Position.BOTTOM_START);
|
|
||||||
refreshGrid(); // when a row is selected but the data is no longer available refresh grid
|
|
||||||
event.forwardTo(UsersView.class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createEditorLayout(SplitLayout splitLayout) {
|
|
||||||
Div editorLayoutDiv = new Div();
|
|
||||||
editorLayoutDiv.setClassName("editor-layout");
|
|
||||||
Div editorDiv = new Div();
|
|
||||||
editorDiv.setClassName("editor");
|
|
||||||
editorLayoutDiv.add(editorDiv);
|
|
||||||
|
|
||||||
FormLayout formLayout = new FormLayout();
|
|
||||||
firstName = new TextField("First Name");
|
|
||||||
lastName = new TextField("Last Name");
|
|
||||||
username = new TextField("Username");
|
|
||||||
password = new PasswordField("Password");
|
|
||||||
email = new EmailField("Email");
|
|
||||||
roles = new MultiSelectComboBox<>("Roles");
|
|
||||||
roles.setItems(Role.values()); // Set the enum values as options
|
|
||||||
roles.setItemLabelGenerator(Role::getRoleName); // Define how to display roles
|
|
||||||
|
|
||||||
// We set the maximum parallel columns to 1
|
|
||||||
formLayout.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1));
|
|
||||||
|
|
||||||
formLayout.add(firstName, lastName, username, password, email, roles);
|
|
||||||
editorDiv.add(formLayout);
|
|
||||||
createButtonLayout(editorLayoutDiv);
|
|
||||||
|
|
||||||
splitLayout.addToSecondary(editorLayoutDiv);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createButtonLayout(Div editorLayoutDiv) {
|
|
||||||
HorizontalLayout buttonLayout = new HorizontalLayout();
|
|
||||||
buttonLayout.setClassName("button-layout");
|
|
||||||
cancel.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
|
||||||
save.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
|
||||||
delete.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
|
||||||
buttonLayout.add(save, delete, cancel);
|
|
||||||
editorLayoutDiv.add(buttonLayout);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createGridLayout(SplitLayout splitLayout) {
|
|
||||||
Div wrapper = new Div();
|
|
||||||
wrapper.setClassName("grid-wrapper");
|
|
||||||
splitLayout.addToPrimary(wrapper);
|
|
||||||
wrapper.add(grid);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void refreshGrid() {
|
|
||||||
grid.select(null);
|
|
||||||
grid.getDataProvider().refreshAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void clearForm() {
|
|
||||||
populateForm(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void populateForm(User value) {
|
|
||||||
this.user = value;
|
|
||||||
binder.readBean(this.user);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,25 +2,12 @@ server.port=${PORT:8080}
|
||||||
logging.level.org.atmosphere = warn
|
logging.level.org.atmosphere = warn
|
||||||
spring.mustache.check-template-location = false
|
spring.mustache.check-template-location = false
|
||||||
|
|
||||||
# Activate the default profile
|
|
||||||
spring.profiles.active=dev
|
|
||||||
|
|
||||||
# Launch the default browser when starting the application in development mode
|
# Launch the default browser when starting the application in development mode
|
||||||
#vaadin.launch-browser=true
|
#vaadin.launch-browser=true
|
||||||
# PostgreSQL configuration.
|
# PostgreSQL configuration.
|
||||||
spring.datasource.url = ${DB_ADDRESS}/${DB_NAME}
|
spring.datasource.url = jdbc:postgresql://localhost:5432/munera_vaadin
|
||||||
spring.datasource.username = ${DB_USER}
|
spring.datasource.username = postgres
|
||||||
spring.datasource.password = ${DB_PASSWORD}
|
spring.datasource.password =
|
||||||
|
|
||||||
# Default user
|
|
||||||
admin.username=${ADMIN_USERNAME}
|
|
||||||
admin.password=${ADMIN_PASSWORD}
|
|
||||||
admin.first_name=${ADMIN_FIRST_NAME}
|
|
||||||
admin.last_name=${ADMIN_LAST_NAME}
|
|
||||||
admin.roles=${ADMIN_ROLES}
|
|
||||||
admin.email=${ADMIN_EMAIL}
|
|
||||||
|
|
||||||
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
|
|
||||||
spring.jpa.hibernate.ddl-auto = update
|
spring.jpa.hibernate.ddl-auto = update
|
||||||
# To improve the performance during development.
|
# To improve the performance during development.
|
||||||
# For more information https://vaadin.com/docs/latest/integrations/spring/configuration#special-configuration-parameters
|
# For more information https://vaadin.com/docs/latest/integrations/spring/configuration#special-configuration-parameters
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 116 KiB |
Binary file not shown.
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 177 KiB |
Binary file not shown.
Before Width: | Height: | Size: 141 KiB |
|
@ -1,93 +0,0 @@
|
||||||
package com.application.munera.services;
|
|
||||||
|
|
||||||
import com.application.munera.data.Expense;
|
|
||||||
import com.application.munera.data.Person;
|
|
||||||
import com.application.munera.repositories.ExpenseRepository;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Disabled;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
|
||||||
import org.mockito.InjectMocks;
|
|
||||||
import org.mockito.Mock;
|
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
|
||||||
import org.mockito.junit.jupiter.MockitoSettings;
|
|
||||||
import org.mockito.quality.Strictness;
|
|
||||||
|
|
||||||
import java.time.Year;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
|
||||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
|
||||||
class ExpenseServiceTest {
|
|
||||||
@Mock
|
|
||||||
private ExpenseRepository expenseRepository;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private Person loggedInPerson;
|
|
||||||
|
|
||||||
@InjectMocks
|
|
||||||
private ExpenseService expenseService;
|
|
||||||
|
|
||||||
private Year year;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
public void setUp() {
|
|
||||||
year = Year.of(2023);
|
|
||||||
when(loggedInPerson.getId()).thenReturn(1L);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testFetchExpensesForDashboard_NoExpenses() {
|
|
||||||
when(expenseRepository.findExpensesByPayerAndBeneficiaryAndYear(loggedInPerson.getId(), year.getValue()))
|
|
||||||
.thenReturn(new ArrayList<>());
|
|
||||||
when(expenseRepository.findExpensesByBeneficiaryAndYear(loggedInPerson.getId(), year.getValue()))
|
|
||||||
.thenReturn(new ArrayList<>());
|
|
||||||
when(expenseRepository.findExpensesByPayerAndYear(loggedInPerson.getId(), year.getValue()))
|
|
||||||
.thenReturn(new ArrayList<>());
|
|
||||||
|
|
||||||
List<Expense> result = expenseService.fetchExpensesForDashboard(loggedInPerson, year);
|
|
||||||
|
|
||||||
assertEquals(0, result.size(), "Expected no expenses to be fetched");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Disabled("will need to become integration test")
|
|
||||||
void testFetchExpensesForDashboard_WithSelfExpenses() {
|
|
||||||
Expense selfExpense = new Expense(); // Create a dummy Expense object
|
|
||||||
List<Expense> bothExpenses = List.of(selfExpense);
|
|
||||||
when(expenseRepository.findExpensesByPayerAndBeneficiaryAndYear(loggedInPerson.getId(), year.getValue()))
|
|
||||||
.thenReturn(bothExpenses);
|
|
||||||
when(expenseRepository.findExpensesByBeneficiaryAndYear(loggedInPerson.getId(), year.getValue()))
|
|
||||||
.thenReturn(new ArrayList<>());
|
|
||||||
when(expenseRepository.findExpensesByPayerAndYear(loggedInPerson.getId(), year.getValue()))
|
|
||||||
.thenReturn(new ArrayList<>());
|
|
||||||
|
|
||||||
List<Expense> result = expenseService.fetchExpensesForDashboard(loggedInPerson, year);
|
|
||||||
|
|
||||||
assertEquals(1, result.size(), "Expected one self-expense to be fetched");
|
|
||||||
assertEquals(selfExpense, result.get(0), "Expected the self-expense to match");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Disabled("will need to become integration test")
|
|
||||||
void testFetchExpensesForDashboard_WithUnpaidExpenses() {
|
|
||||||
Expense unpaidExpense = new Expense();
|
|
||||||
unpaidExpense.setIsPaid(false);
|
|
||||||
when(expenseRepository.findExpensesByPayerAndBeneficiaryAndYear(loggedInPerson.getId(), year.getValue()))
|
|
||||||
.thenReturn(new ArrayList<>());
|
|
||||||
when(expenseRepository.findExpensesByBeneficiaryAndYear(loggedInPerson.getId(), year.getValue()))
|
|
||||||
.thenReturn(new ArrayList<>());
|
|
||||||
when(expenseRepository.findExpensesByPayerAndYear(loggedInPerson.getId(), year.getValue()))
|
|
||||||
.thenReturn(List.of(unpaidExpense));
|
|
||||||
|
|
||||||
List<Expense> result = expenseService.fetchExpensesForDashboard(loggedInPerson, year);
|
|
||||||
|
|
||||||
assertEquals(1, result.size(), "Expected one unpaid expense to be fetched");
|
|
||||||
assertEquals(unpaidExpense, result.getFirst(), "Expected the unpaid expense to match");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,148 +0,0 @@
|
||||||
package com.application.munera.services;
|
|
||||||
|
|
||||||
import com.application.munera.data.Expense;
|
|
||||||
import com.application.munera.data.Person;
|
|
||||||
import com.application.munera.repositories.ExpenseRepository;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
|
||||||
import org.mockito.InjectMocks;
|
|
||||||
import org.mockito.Mock;
|
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
|
||||||
import org.mockito.junit.jupiter.MockitoSettings;
|
|
||||||
import org.mockito.quality.Strictness;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
import static org.mockito.Mockito.mock;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
|
||||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
|
||||||
class PersonServiceTest {
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private ExpenseRepository expenseRepository;
|
|
||||||
@InjectMocks
|
|
||||||
private PersonService personService;
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void calculateDeb_whenExpensesAreMixed() {
|
|
||||||
Person person = new Person();
|
|
||||||
person.setLastName("first");
|
|
||||||
person.setFirstName("person");
|
|
||||||
person.setId(1L);
|
|
||||||
|
|
||||||
Person person2 = new Person();
|
|
||||||
person2.setLastName("second");
|
|
||||||
person2.setFirstName("person");
|
|
||||||
|
|
||||||
Expense expense1 = mock(Expense.class);
|
|
||||||
when(expense1.getPayer()).thenReturn(person);
|
|
||||||
when(expense1.getBeneficiary()).thenReturn(person2);
|
|
||||||
when(expense1.getCost()).thenReturn(new BigDecimal("110.00"));
|
|
||||||
when(expense1.getIsPaid()).thenReturn(false);
|
|
||||||
|
|
||||||
Expense expense2 = mock(Expense.class);
|
|
||||||
when(expense2.getPayer()).thenReturn(person);
|
|
||||||
when(expense2.getBeneficiary()).thenReturn(person);
|
|
||||||
when(expense2.getCost()).thenReturn(new BigDecimal("50.00"));
|
|
||||||
when(expense2.getIsPaid()).thenReturn(false);
|
|
||||||
|
|
||||||
Expense expense3 = mock(Expense.class);
|
|
||||||
when(expense3.getPayer()).thenReturn(person);
|
|
||||||
when(expense3.getBeneficiary()).thenReturn(person);
|
|
||||||
when(expense3.getCost()).thenReturn(new BigDecimal("50.00"));
|
|
||||||
when(expense3.getIsPaid()).thenReturn(true);
|
|
||||||
|
|
||||||
when(expenseRepository.findExpensesByPayer(person.getId())).thenReturn(List.of(expense1, expense2, expense3));
|
|
||||||
|
|
||||||
BigDecimal totalDebt = personService.calculateDebt(person);
|
|
||||||
|
|
||||||
assertEquals(new BigDecimal("110.00"), totalDebt);
|
|
||||||
}
|
|
||||||
@Test
|
|
||||||
void calculateDebt_NoExpenses() {
|
|
||||||
// Arrange
|
|
||||||
Person person = new Person();
|
|
||||||
person.setId(1L);
|
|
||||||
when(expenseRepository.findExpensesByPayer(person.getId())).thenReturn(Collections.emptyList());
|
|
||||||
|
|
||||||
// Act
|
|
||||||
BigDecimal totalDebt = personService.calculateDebt(person);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
assertEquals(BigDecimal.ZERO, totalDebt);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void calculateDebt_AllExpensesPaid() {
|
|
||||||
// Arrange
|
|
||||||
Person person = new Person();
|
|
||||||
person.setId(1L);
|
|
||||||
|
|
||||||
Expense expense1 = mock(Expense.class);
|
|
||||||
when(expense1.getPayer()).thenReturn(person);
|
|
||||||
when(expense1.getBeneficiary()).thenReturn(new Person());
|
|
||||||
when(expense1.getCost()).thenReturn(new BigDecimal("100.00"));
|
|
||||||
when(expense1.getIsPaid()).thenReturn(true);
|
|
||||||
|
|
||||||
Expense expense2 = mock(Expense.class);
|
|
||||||
when(expense2.getPayer()).thenReturn(person);
|
|
||||||
when(expense2.getBeneficiary()).thenReturn(new Person());
|
|
||||||
when(expense2.getCost()).thenReturn(new BigDecimal("50.00"));
|
|
||||||
when(expense2.getIsPaid()).thenReturn(true);
|
|
||||||
|
|
||||||
when(expenseRepository.findExpensesByPayer(person.getId())).thenReturn(List.of(expense1, expense2));
|
|
||||||
|
|
||||||
// Act
|
|
||||||
BigDecimal totalDebt = personService.calculateDebt(person);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
assertEquals(BigDecimal.ZERO, totalDebt);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void calculateDebt_ExpensesWithSamePayerAndBeneficiary() {
|
|
||||||
// Arrange
|
|
||||||
Person person = new Person();
|
|
||||||
person.setId(1L);
|
|
||||||
|
|
||||||
Expense expense1 = mock(Expense.class);
|
|
||||||
when(expense1.getPayer()).thenReturn(person);
|
|
||||||
when(expense1.getBeneficiary()).thenReturn(person); // Same person as payer
|
|
||||||
when(expense1.getCost()).thenReturn(new BigDecimal("100.00"));
|
|
||||||
when(expense1.getIsPaid()).thenReturn(false);
|
|
||||||
|
|
||||||
when(expenseRepository.findExpensesByPayer(person.getId())).thenReturn(List.of(expense1));
|
|
||||||
|
|
||||||
// Act
|
|
||||||
BigDecimal totalDebt = personService.calculateDebt(person);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
assertEquals(BigDecimal.ZERO, totalDebt);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void calculateDebt_ExpensesWithNullAttributes() {
|
|
||||||
// Arrange
|
|
||||||
Person person = new Person();
|
|
||||||
person.setId(1L);
|
|
||||||
|
|
||||||
Expense expense1 = mock(Expense.class);
|
|
||||||
when(expense1.getPayer()).thenReturn(person);
|
|
||||||
when(expense1.getBeneficiary()).thenReturn(new Person());
|
|
||||||
when(expense1.getCost()).thenReturn(null); // Null cost
|
|
||||||
when(expense1.getIsPaid()).thenReturn(false);
|
|
||||||
|
|
||||||
when(expenseRepository.findExpensesByPayer(person.getId())).thenReturn(List.of(expense1));
|
|
||||||
|
|
||||||
// Act
|
|
||||||
BigDecimal totalDebt = personService.calculateDebt(person);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
assertEquals(BigDecimal.ZERO, totalDebt);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
// This TypeScript configuration file is generated by vaadin-maven-plugin.
|
|
||||||
// This is needed for TypeScript compiler to compile your TypeScript code in the project.
|
|
||||||
// It is recommended to commit this file to the VCS.
|
|
||||||
// You might want to change the configurations to fit your preferences
|
|
||||||
// For more information about the configurations, please refer to http://www.typescriptlang.org/docs/handbook/tsconfig-json.html
|
|
||||||
{
|
|
||||||
"_version": "9",
|
|
||||||
"compilerOptions": {
|
|
||||||
"sourceMap": true,
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"inlineSources": true,
|
|
||||||
"module": "esNext",
|
|
||||||
"target": "es2020",
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"strict": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"noImplicitReturns": true,
|
|
||||||
"noImplicitAny": true,
|
|
||||||
"noImplicitThis": true,
|
|
||||||
"noUnusedLocals": false,
|
|
||||||
"noUnusedParameters": false,
|
|
||||||
"experimentalDecorators": true,
|
|
||||||
"useDefineForClassFields": false,
|
|
||||||
"baseUrl": "frontend",
|
|
||||||
"paths": {
|
|
||||||
"@vaadin/flow-frontend": ["generated/jar-resources"],
|
|
||||||
"@vaadin/flow-frontend/*": ["generated/jar-resources/*"],
|
|
||||||
"Frontend/*": ["*"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"frontend/**/*",
|
|
||||||
"types.d.ts"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"frontend/generated/jar-resources/**"
|
|
||||||
]
|
|
||||||
}
|
|
10
types.d.ts
vendored
10
types.d.ts
vendored
|
@ -1,10 +0,0 @@
|
||||||
// This TypeScript modules definition file is generated by vaadin-maven-plugin.
|
|
||||||
// You can not directly import your different static files into TypeScript,
|
|
||||||
// This is needed for TypeScript compiler to declare and export as a TypeScript module.
|
|
||||||
// It is recommended to commit this file to the VCS.
|
|
||||||
// You might want to change the configurations to fit your preferences
|
|
||||||
declare module '*.css' {
|
|
||||||
import { CSSResultGroup } from 'lit';
|
|
||||||
const content: CSSResultGroup;
|
|
||||||
export default content;
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { UserConfigFn } from 'vite';
|
|
||||||
import { overrideVaadinConfig } from './vite.generated';
|
|
||||||
|
|
||||||
const customConfig: UserConfigFn = (env) => ({
|
|
||||||
// Here you can add custom Vite parameters
|
|
||||||
// https://vitejs.dev/config/
|
|
||||||
});
|
|
||||||
|
|
||||||
export default overrideVaadinConfig(customConfig);
|
|
Loading…
Reference in a new issue