Erweiterungen

This commit is contained in:
2025-08-22 10:01:37 +02:00
parent 8644373022
commit 545fce0a50
4 changed files with 310 additions and 20 deletions

View File

@@ -99,7 +99,7 @@ public final class MainLayout extends AppLayout {
// Create navigation items for the collapsible list
SideNavItem profile = new SideNavItem("Mein Profil", "edit-profile", new Icon(VaadinIcon.USER));
SideNavItem myInvoices = new SideNavItem("Meine Rechnungen", "8", new Icon(VaadinIcon.COG));
SideNavItem myInvoices = new SideNavItem("Meine Rechnungen", "my-invoices", new Icon(VaadinIcon.FILE_TEXT));
SideNavItem imprint = new SideNavItem("Impressum", "impressum", new Icon(VaadinIcon.INFO_CIRCLE));
userContent.add(profile, myInvoices, imprint);

View File

@@ -656,7 +656,7 @@ public class AddJobView extends Main {
// Bind customerSelection field with validation
binder.forField(customerSelection)
.asRequired("Auftraggeber/Rechnungsempfänger ist erforderlich")
.asRequired("")
.bind(Job::getCustomerSelection, Job::setCustomerSelection);
// Bind optional fields without validation

View File

@@ -9,28 +9,47 @@ import com.vaadin.flow.component.html.Paragraph;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.EmailField;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.data.validator.EmailValidator;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import de.assecutor.votianlt.model.User;
import de.assecutor.votianlt.pages.service.UserService;
import de.assecutor.votianlt.security.SecurityService;
import jakarta.annotation.security.RolesAllowed;
@PageTitle("Profil bearbeiten")
@Route(value = "edit-profile", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@RolesAllowed({"USER","ADMIN"})
public class EditProfileView extends HorizontalLayout {
public EditProfileView() {
private final UserService userService;
private final SecurityService securityService;
private final Binder<User> binder = new Binder<>(User.class);
private final User currentUser;
public EditProfileView(UserService userService, SecurityService securityService) {
this.userService = userService;
this.securityService = securityService;
this.currentUser = securityService.getCurrentDatabaseUser();
setSizeFull();
setPadding(true);
setSpacing(true);
setJustifyContentMode(JustifyContentMode.CENTER);
// 2% Abstand zwischen den Spalten
setWidthFull();
setSpacing(false);
getStyle().set("column-gap", "2%");
setAlignItems(Alignment.START);
// Linke Spalte: Formular
VerticalLayout formColumn = new VerticalLayout();
formColumn.setWidth("500px");
formColumn.setWidth("48%");
formColumn.setPadding(false);
formColumn.setSpacing(false);
@@ -39,19 +58,19 @@ public class EditProfileView extends HorizontalLayout {
form.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 2));
// Firmenfelder
TextField companyField = new TextField("Firma*");
TextField companyField = new TextField("Firma");
TextField companyAddField = new TextField("Firmenzusatz");
TextField firstnameField = new TextField("Vorname*");
TextField lastnameField = new TextField("Nachname*");
TextField phoneField = new TextField("Telefonnummer*");
TextField firstnameField = new TextField("Vorname");
TextField lastnameField = new TextField("Nachname");
TextField phoneField = new TextField("Telefonnummer");
TextField faxField = new TextField("Telefon (Fax)");
TextField mobileField = new TextField("Telefon (Mobil)");
EmailField emailField = new EmailField("E-Mail-Adresse (Login)*");
TextField streetField = new TextField("Straße*");
TextField houseNumberField = new TextField("Hausnr*");
TextField streetField = new TextField("Straße");
TextField houseNumberField = new TextField("Hausnr");
TextField addressAddField = new TextField("Adresszusatz");
TextField zipField = new TextField("Postleitzahl*");
TextField cityField = new TextField("Stadt*");
TextField zipField = new TextField("Postleitzahl");
TextField cityField = new TextField("Stadt");
// Pflichtfeldhinweis
Paragraph pflichtHinweis = new Paragraph("Die mit (*) gekennzeichneten Felder sind Pflichtfelder.");
@@ -61,15 +80,15 @@ public class EditProfileView extends HorizontalLayout {
Checkbox diffInvoiceAddress = new Checkbox("Abweichende Rechnungsadresse");
diffInvoiceAddress.getStyle().set("marginTop", "1em");
// Rechnungsadresse Felder (disabled by default)
TextField invCompanyField = new TextField("Firma*");
TextField invCompanyField = new TextField("Firma");
TextField invCompanyAddField = new TextField("Firmenzusatz");
TextField invFirstnameField = new TextField("Vorname*");
TextField invLastnameField = new TextField("Nachname*");
TextField invStreetField = new TextField("Straße*");
TextField invHouseNumberField = new TextField("Hausnr*");
TextField invFirstnameField = new TextField("Vorname");
TextField invLastnameField = new TextField("Nachname");
TextField invStreetField = new TextField("Straße");
TextField invHouseNumberField = new TextField("Hausnr");
TextField invAddressAddField = new TextField("Adresszusatz");
TextField invZipField = new TextField("Postleitzahl*");
TextField invCityField = new TextField("Stadt*");
TextField invZipField = new TextField("Postleitzahl");
TextField invCityField = new TextField("Stadt");
invCompanyField.setEnabled(false);
invCompanyAddField.setEnabled(false);
invFirstnameField.setEnabled(false);
@@ -79,6 +98,17 @@ public class EditProfileView extends HorizontalLayout {
invAddressAddField.setEnabled(false);
invZipField.setEnabled(false);
invCityField.setEnabled(false);
// Felder initial ausblenden
invCompanyField.setVisible(false);
invCompanyAddField.setVisible(false);
invFirstnameField.setVisible(false);
invLastnameField.setVisible(false);
invStreetField.setVisible(false);
invHouseNumberField.setVisible(false);
invAddressAddField.setVisible(false);
invZipField.setVisible(false);
invCityField.setVisible(false);
diffInvoiceAddress.addValueChangeListener(e -> {
boolean enabled = e.getValue();
invCompanyField.setEnabled(enabled);
@@ -88,6 +118,17 @@ public class EditProfileView extends HorizontalLayout {
invStreetField.setEnabled(enabled);
invHouseNumberField.setEnabled(enabled);
invAddressAddField.setEnabled(enabled);
// Sichtbarkeit an Checkbox koppeln
invCompanyField.setVisible(enabled);
invCompanyAddField.setVisible(enabled);
invFirstnameField.setVisible(enabled);
invLastnameField.setVisible(enabled);
invStreetField.setVisible(enabled);
invHouseNumberField.setVisible(enabled);
invAddressAddField.setVisible(enabled);
invZipField.setVisible(enabled);
invCityField.setVisible(enabled);
invZipField.setEnabled(enabled);
invCityField.setEnabled(enabled);
});
@@ -99,6 +140,54 @@ public class EditProfileView extends HorizontalLayout {
form.add(phoneField, faxField);
form.add(mobileField, 2);
form.add(emailField, 2);
// Binder: Pflichtfelder und Bindings
// UI-Pflichtfelder ohne Datenbindung (da nicht im User-Modell vorhanden)
companyField.setRequiredIndicatorVisible(true);
streetField.setRequiredIndicatorVisible(true);
houseNumberField.setRequiredIndicatorVisible(true);
zipField.setRequiredIndicatorVisible(true);
cityField.setRequiredIndicatorVisible(true);
binder.forField(companyField)
.asRequired("")
.bind(user -> null, (user, v) -> {});
binder.forField(streetField)
.asRequired("")
.bind(user -> null, (user, v) -> {});
binder.forField(houseNumberField)
.asRequired("")
.bind(user -> null, (user, v) -> {});
binder.forField(zipField)
.asRequired("")
.bind(user -> null, (user, v) -> {});
binder.forField(cityField)
.asRequired("")
.bind(user -> null, (user, v) -> {});
binder.forField(firstnameField)
.asRequired("")
.bind(User::getFirstname, User::setFirstname);
binder.forField(lastnameField)
.asRequired("")
.bind(User::getName, User::setName);
binder.forField(phoneField)
.asRequired("")
.bind(User::getPhone, User::setPhone);
binder.forField(emailField)
.asRequired("")
.withValidator(new EmailValidator("Ungültige E-Mail-Adresse"))
.bind(User::getEmail, User::setEmail);
// Optionale Felder
binder.forField(mobileField).bind(User::getPhone2, User::setPhone2);
binder.forField(faxField).bind(User::getFax, User::setFax);
// Pflichtindikator sichtbar machen
firstnameField.setRequiredIndicatorVisible(true);
lastnameField.setRequiredIndicatorVisible(true);
phoneField.setRequiredIndicatorVisible(true);
emailField.setRequiredIndicatorVisible(true);
// Aktuellen Benutzer in die Felder laden
binder.readBean(currentUser);
form.add(streetField, houseNumberField);
form.add(addressAddField, 2);
form.add(zipField, cityField);
@@ -120,7 +209,7 @@ public class EditProfileView extends HorizontalLayout {
// Rechte Spalte: Karte und Aktionen
VerticalLayout rightColumn = new VerticalLayout();
rightColumn.setWidth("100%");
rightColumn.setWidth("48%");
rightColumn.setAlignItems(Alignment.CENTER);
rightColumn.setSpacing(true);
rightColumn.setPadding(false);
@@ -160,6 +249,18 @@ public class EditProfileView extends HorizontalLayout {
// Profil speichern Button (unten rechts)
Button saveProfile = new Button("Profiländerungen speichern");
saveProfile.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
saveProfile.addClickListener(e -> {
if (binder.validate().isOk()) {
try {
binder.writeBean(currentUser);
userService.save(currentUser);
Notification.show("Profil gespeichert", 3000, Notification.Position.BOTTOM_END);
} catch (Exception ex) {
Notification.show("Fehler beim Speichern: " + ex.getMessage(), 4000, Notification.Position.MIDDLE);
}
}
});
saveProfile.getStyle().set("position", "absolute").set("right", "2em").set("bottom", "2em");
add(formColumn, rightColumn, saveProfile);
}

View File

@@ -0,0 +1,189 @@
package de.assecutor.votianlt.pages.view;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.html.*;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.select.Select;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.value.ValueChangeMode;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar;
import de.assecutor.votianlt.pages.base.ui.view.MainLayout;
import jakarta.annotation.security.RolesAllowed;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
/**
* Meine Rechnungen nutzerzentrierte Übersicht.
*
* Layout orientiert am bereitgestellten Screenshot:
* - Zwei Karten oben (Offene Rechnungen, Bankverbindung)
* - Darunter ein Bereich „Rechnungen" mit Grid, Suche und Seitengröße
*/
@PageTitle("Meine Rechnungen")
@Route(value = "my-invoices", layout = MainLayout.class)
@RolesAllowed("USER")
public class MyInvoicesView extends Main {
private final Grid<MyInvoiceRow> grid = new Grid<>(MyInvoiceRow.class, false);
private final List<MyInvoiceRow> allRows = new ArrayList<>(); // zunächst leer
public MyInvoicesView() {
getStyle().set("max-width", "90%");
getStyle().set("margin-left", "auto");
getStyle().set("margin-right", "auto");
// Toolbar / Titel
add(new ViewToolbar("Meine Rechnungen"));
// Kartenbereich oben
add(createTopCards());
// Rechnungsbereich unten
add(createInvoicesSection());
}
private Component createTopCards() {
// Container mit zwei Spalten (responsiv)
Div container = new Div();
container.getStyle()
.set("display", "grid")
.set("grid-template-columns", "48% 2% 48%");
//.set("gap", "10px");
// Spaltenabstände: 2% zwischen den beiden Spalten
container.getStyle().set("column-gap", "0");
// Karte: Offene Rechnungen
Paragraph hint = new Paragraph("Momentan sind keine neuen Rechnungen für Sie im System gespeichert.");
hint.getStyle().set("color", "var(--lumo-success-text-color)");
Div openInvoicesCard = createCard("Offene Rechnungen", hint);
// Karte: Bankverbindung
VerticalLayout bankData = new VerticalLayout();
bankData.setPadding(false);
bankData.setSpacing(false);
bankData.add(
labeledValue("Kreditinstitut", "Hamburger Sparkasse"),
labeledValue("Begünstigter", "Assecutor Data Service GmbH"),
labeledValue("IBAN", "DE67200505501217139888"),
labeledValue("Verwendungszweck", "vlt-00000610")
);
Div bankCard = createCard("Bankverbindung", bankData);
container.add(openInvoicesCard, bankCard);
return container;
}
private Component createInvoicesSection() {
Div card = new Div();
// Abstand zur oberen Zeile
card.getStyle().set("margin-top", "30px");
styleCard(card);
// Kopfzeile
H3 title = new H3("Rechnungen");
title.getStyle().set("margin", "0");
// Steuerleiste: Seitengröße + Suche
Select<Integer> pageSize = new Select<>();
pageSize.setItems(10, 25, 50);
pageSize.setLabel("Einträge anzeigen");
pageSize.setValue(10);
pageSize.setWidth("140px");
TextField search = new TextField();
search.setLabel("Suchen");
search.setClearButtonVisible(true);
search.setValueChangeMode(ValueChangeMode.EAGER);
// Layout Kopf + Controls
HorizontalLayout header = new HorizontalLayout();
header.setWidthFull();
header.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.END);
header.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN);
header.add(title, new Div()); // Platzhalter zum Strecken
HorizontalLayout controls = new HorizontalLayout(pageSize, search);
controls.setWidthFull();
controls.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN);
// Grid konfigurieren
grid.addColumn(MyInvoiceRow::status).setHeader("Status").setAutoWidth(true);
grid.addColumn(MyInvoiceRow::invoiceNumber).setHeader("Rechnungsnummer").setAutoWidth(true);
grid.addColumn(MyInvoiceRow::date).setHeader("Datum").setAutoWidth(true);
grid.addColumn(MyInvoiceRow::amount).setHeader("Betrag").setAutoWidth(true);
grid.setAllRowsVisible(true);
grid.setItems(allRows); // zunächst leer
// Suche (einfacher Text-Filter über alle sichtbaren Felder)
search.addValueChangeListener(e -> applyFilter(e.getValue()));
// Paginierungs-Buttons (vorerst ohne Funktion, als Platzhalter)
Button prev = new Button("Zurück", VaadinIcon.ANGLE_LEFT.create());
Button next = new Button("Nächste", VaadinIcon.ANGLE_RIGHT.create());
prev.setEnabled(false);
next.setEnabled(false);
HorizontalLayout pager = new HorizontalLayout(prev, next);
pager.setWidthFull();
pager.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
// Zusammenbauen
card.add(header, controls, grid, pager);
return card;
}
private void applyFilter(String filter) {
String f = filter == null ? "" : filter.toLowerCase();
grid.setItems(allRows.stream().filter(row ->
row.status.toLowerCase().contains(f)
|| row.invoiceNumber.toLowerCase().contains(f)
|| row.date.toString().toLowerCase().contains(f)
|| String.valueOf(row.amount).toLowerCase().contains(f)
).toList());
}
private Div createCard(String title, Component content) {
Div card = new Div();
styleCard(card);
H3 h3 = new H3(title);
h3.getStyle().set("margin", "0");
Div inner = new Div(content);
inner.getStyle().set("padding", "var(--lumo-space-s)");
card.add(h3, inner);
return card;
}
private Component labeledValue(String label, String value) {
Paragraph p = new Paragraph();
Span sLabel = new Span(label + " ");
sLabel.getStyle().set("font-weight", "600");
Span sValue = new Span(value);
p.add(sLabel, sValue);
p.getStyle().set("margin", "0");
return p;
}
private void styleCard(Div card) {
card.getStyle()
.set("border", "1px solid var(--lumo-contrast-20pct)")
.set("border-radius", "var(--lumo-border-radius-l)")
.set("padding", "var(--lumo-space-m)")
.set("background", "var(--lumo-base-color)")
.set("box-shadow", "0 1px 1px rgba(0,0,0,0.02)");
card.setWidthFull();
}
// Schlanke lokale Repräsentation für das Grid
public record MyInvoiceRow(String status, String invoiceNumber, LocalDate date, double amount) {}
}