diff --git a/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java b/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java index 4f6f9a5..6a6b415 100644 --- a/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java +++ b/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java @@ -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); diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java b/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java index b08955b..7b86ebd 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java @@ -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 diff --git a/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java b/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java index a1ec583..a6a246d 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java @@ -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 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); } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/MyInvoicesView.java b/src/main/java/de/assecutor/votianlt/pages/view/MyInvoicesView.java new file mode 100644 index 0000000..dd31a39 --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/pages/view/MyInvoicesView.java @@ -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 grid = new Grid<>(MyInvoiceRow.class, false); + private final List 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 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) {} +} +