From 1a5ea5d47a55dfdcfa3792569674d98af7f64794 Mon Sep 17 00:00:00 2001 From: Sven Carstensen Date: Tue, 23 Sep 2025 10:28:19 +0200 Subject: [PATCH] Erweiterungen --- .../de/assecutor/votianlt/model/User.java | 13 +++ .../votianlt/model/UserInvoiceData.java | 42 +++++++ .../pages/base/ui/view/MainLayout.java | 32 +++++- .../pages/service/UserInvoiceDataService.java | 57 ++++++++++ .../votianlt/pages/view/EditProfileView.java | 106 +++++++++++++++--- .../repository/UserInvoiceDataRepository.java | 16 +++ 6 files changed, 246 insertions(+), 20 deletions(-) create mode 100644 src/main/java/de/assecutor/votianlt/model/UserInvoiceData.java create mode 100644 src/main/java/de/assecutor/votianlt/pages/service/UserInvoiceDataService.java create mode 100644 src/main/java/de/assecutor/votianlt/repository/UserInvoiceDataRepository.java diff --git a/src/main/java/de/assecutor/votianlt/model/User.java b/src/main/java/de/assecutor/votianlt/model/User.java index 675bdbf..2c05ac3 100644 --- a/src/main/java/de/assecutor/votianlt/model/User.java +++ b/src/main/java/de/assecutor/votianlt/model/User.java @@ -23,12 +23,25 @@ public class User { // Firmen-/Adressdaten private String company; // Firma + private String companyAddition; // Firmenzusatz private String street; // Straße private String houseNumber; // Hausnr private String addressAddition; // Adresszusatz (optional) private String zip; // Postleitzahl private String city; // Stadt + // Abweichende Rechnungsadresse + private boolean diffInvoiceAddress; // Checkbox für abweichende Rechnungsadresse + private String invCompany; // Rechnungsadresse: Firma + private String invCompanyAddition; // Rechnungsadresse: Firmenzusatz + private String invFirstname; // Rechnungsadresse: Vorname + private String invLastname; // Rechnungsadresse: Nachname + private String invStreet; // Rechnungsadresse: Straße + private String invHouseNumber; // Rechnungsadresse: Hausnr + private String invAddressAddition; // Rechnungsadresse: Adresszusatz + private String invZip; // Rechnungsadresse: Postleitzahl + private String invCity; // Rechnungsadresse: Stadt + @Indexed(unique = true) private String email; diff --git a/src/main/java/de/assecutor/votianlt/model/UserInvoiceData.java b/src/main/java/de/assecutor/votianlt/model/UserInvoiceData.java new file mode 100644 index 0000000..398e7a3 --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/model/UserInvoiceData.java @@ -0,0 +1,42 @@ +package de.assecutor.votianlt.model; + +import lombok.Data; +import org.bson.types.ObjectId; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.index.Indexed; + +import java.time.LocalDateTime; + +@Data +@Document(collection = "user_invoice_data") +public class UserInvoiceData { + + @Id + private ObjectId id; + + @Indexed + private ObjectId userId; + + private boolean billingEnabled; + private String prefix; + private String ustId; + private String taxNumber; + private String bankName; + private String iban; + private String taxRate; + private String introText; + private String paymentTerms; + + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public UserInvoiceData() { + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } + + public void updateTimestamp() { + this.updatedAt = LocalDateTime.now(); + } +} \ No newline at end of file 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 ea2be4e..7115fd6 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 @@ -20,6 +20,9 @@ import com.vaadin.flow.router.Layout; import com.vaadin.flow.server.auth.AnonymousAllowed; import com.vaadin.flow.server.menu.MenuConfiguration; import com.vaadin.flow.server.menu.MenuEntry; +import de.assecutor.votianlt.model.User; +import de.assecutor.votianlt.model.UserInvoiceData; +import de.assecutor.votianlt.pages.service.UserInvoiceDataService; import de.assecutor.votianlt.pages.view.EditProfileView; import de.assecutor.votianlt.security.SecurityService; @@ -31,12 +34,14 @@ import static com.vaadin.flow.theme.lumo.LumoUtility.*; public final class MainLayout extends AppLayout { private final SecurityService securityService; + private final UserInvoiceDataService userInvoiceDataService; private Div headerRef; private Scroller navRef; private Component userMenuRef; - public MainLayout(SecurityService securityService) { + public MainLayout(SecurityService securityService, UserInvoiceDataService userInvoiceDataService) { this.securityService = securityService; + this.userInvoiceDataService = userInvoiceDataService; setPrimarySection(Section.DRAWER); // Always build the drawer; keep references and toggle visibility on attach and @@ -102,10 +107,17 @@ public final class MainLayout extends AppLayout { SideNavItem customers = new SideNavItem("Kunden", "customers", new Icon(VaadinIcon.USERS)); SideNavItem appUsers = new SideNavItem("App-Nutzer", "app-user", new Icon(VaadinIcon.USERS)); SideNavItem devices = new SideNavItem("Endgeräte", "app-devices", new Icon(VaadinIcon.MOBILE)); - SideNavItem invoices = new SideNavItem("Rechnungen", "invoices", new Icon(VaadinIcon.FILE_TEXT)); SideNavItem statistics = new SideNavItem("Statistiken", "statistics", new Icon(VaadinIcon.BAR_CHART)); - verwaltungContent.add(jobs, customers, appUsers, devices, invoices, statistics); + verwaltungContent.add(jobs, customers, appUsers, devices); + + // Only show invoices menu if billing is enabled for the current user + if (isBillingEnabledForCurrentUser()) { + SideNavItem invoices = new SideNavItem("Rechnungen", "invoices", new Icon(VaadinIcon.FILE_TEXT)); + verwaltungContent.add(invoices); + } + + verwaltungContent.add(statistics); verwaltungDetails.add(verwaltungContent); // Create Details component for "Verwaltung" with collapsible list @@ -179,4 +191,18 @@ public final class MainLayout extends AppLayout { return userMenu; } + private boolean isBillingEnabledForCurrentUser() { + try { + User currentUser = securityService.getCurrentDatabaseUser(); + if (currentUser != null && currentUser.getId() != null) { + UserInvoiceData invoiceData = userInvoiceDataService.findByUserId(currentUser.getId()).orElse(null); + return invoiceData != null && invoiceData.isBillingEnabled(); + } + } catch (Exception e) { + // Log error or handle appropriately + // Return false as safe default if we can't determine billing status + } + return false; + } + } diff --git a/src/main/java/de/assecutor/votianlt/pages/service/UserInvoiceDataService.java b/src/main/java/de/assecutor/votianlt/pages/service/UserInvoiceDataService.java new file mode 100644 index 0000000..e28a66a --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/pages/service/UserInvoiceDataService.java @@ -0,0 +1,57 @@ +package de.assecutor.votianlt.pages.service; + +import de.assecutor.votianlt.model.UserInvoiceData; +import de.assecutor.votianlt.repository.UserInvoiceDataRepository; +import org.bson.types.ObjectId; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class UserInvoiceDataService { + + private final UserInvoiceDataRepository userInvoiceDataRepository; + + public UserInvoiceDataService(UserInvoiceDataRepository userInvoiceDataRepository) { + this.userInvoiceDataRepository = userInvoiceDataRepository; + } + + public Optional findByUserId(ObjectId userId) { + return userInvoiceDataRepository.findByUserId(userId); + } + + public UserInvoiceData save(UserInvoiceData userInvoiceData) { + userInvoiceData.updateTimestamp(); + return userInvoiceDataRepository.save(userInvoiceData); + } + + public UserInvoiceData createOrUpdate(ObjectId userId, boolean billingEnabled, String prefix, String ustId, + String taxNumber, String bankName, String iban, String taxRate, + String introText, String paymentTerms) { + // If billing is disabled, delete any existing record and return null + if (!billingEnabled) { + deleteByUserId(userId); + return null; + } + + // Otherwise, create or update the record + UserInvoiceData invoiceData = findByUserId(userId).orElse(new UserInvoiceData()); + + invoiceData.setUserId(userId); + invoiceData.setBillingEnabled(billingEnabled); + invoiceData.setPrefix(prefix); + invoiceData.setUstId(ustId); + invoiceData.setTaxNumber(taxNumber); + invoiceData.setBankName(bankName); + invoiceData.setIban(iban); + invoiceData.setTaxRate(taxRate); + invoiceData.setIntroText(introText); + invoiceData.setPaymentTerms(paymentTerms); + + return save(invoiceData); + } + + public void deleteByUserId(ObjectId userId) { + userInvoiceDataRepository.deleteByUserId(userId); + } +} \ No newline at end of file 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 0e3d74d..ff52ab2 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java @@ -13,6 +13,7 @@ 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.UI; import com.vaadin.flow.component.textfield.EmailField; import com.vaadin.flow.component.textfield.TextField; import java.io.ByteArrayOutputStream; @@ -26,7 +27,9 @@ import com.vaadin.flow.data.value.ValueChangeMode; import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.Route; import de.assecutor.votianlt.model.User; +import de.assecutor.votianlt.model.UserInvoiceData; import de.assecutor.votianlt.pages.service.UserService; +import de.assecutor.votianlt.pages.service.UserInvoiceDataService; import de.assecutor.votianlt.security.SecurityService; import jakarta.annotation.security.RolesAllowed; @@ -46,8 +49,12 @@ public class EditProfileView extends HorizontalLayout { private final Binder binder = new Binder<>(User.class); private final User currentUser; + private final UserInvoiceDataService userInvoiceDataService; + private UserInvoiceData currentInvoiceData; + private Checkbox billingEnabled; - public EditProfileView(UserService userService, SecurityService securityService) { + public EditProfileView(UserService userService, UserInvoiceDataService userInvoiceDataService, SecurityService securityService) { + this.userInvoiceDataService = userInvoiceDataService; this.currentUser = securityService.getCurrentDatabaseUser(); setSizeFull(); setPadding(true); @@ -155,32 +162,43 @@ public class EditProfileView extends HorizontalLayout { form.add(mobileField, 2); form.add(emailField, 2); // Binder: Pflichtfelder und Bindings - // UI-Pflichtfelder ohne Datenbindung (da nicht im User-Modell vorhanden) + // Pflichtfelder markieren 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) -> { - }); + // Hauptadresse binden + binder.forField(companyField).asRequired("Firma ist erforderlich").bind(User::getCompany, User::setCompany); + binder.forField(companyAddField).bind(User::getCompanyAddition, User::setCompanyAddition); + binder.forField(streetField).asRequired("Straße ist erforderlich").bind(User::getStreet, User::setStreet); + binder.forField(houseNumberField).asRequired("Hausnummer ist erforderlich").bind(User::getHouseNumber, User::setHouseNumber); + binder.forField(addressAddField).bind(User::getAddressAddition, User::setAddressAddition); + binder.forField(zipField).asRequired("Postleitzahl ist erforderlich").bind(User::getZip, User::setZip); + binder.forField(cityField).asRequired("Stadt ist erforderlich").bind(User::getCity, User::setCity); - 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")) + // Personendaten binden + binder.forField(firstnameField).asRequired("Vorname ist erforderlich").bind(User::getFirstname, User::setFirstname); + binder.forField(lastnameField).asRequired("Nachname ist erforderlich").bind(User::getName, User::setName); + binder.forField(phoneField).asRequired("Telefonnummer ist erforderlich").bind(User::getPhone, User::setPhone); + binder.forField(emailField).asRequired("E-Mail ist erforderlich").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); + + // Abweichende Rechnungsadresse binden + binder.forField(diffInvoiceAddress).bind(User::isDiffInvoiceAddress, User::setDiffInvoiceAddress); + binder.forField(invCompanyField).bind(User::getInvCompany, User::setInvCompany); + binder.forField(invCompanyAddField).bind(User::getInvCompanyAddition, User::setInvCompanyAddition); + binder.forField(invFirstnameField).bind(User::getInvFirstname, User::setInvFirstname); + binder.forField(invLastnameField).bind(User::getInvLastname, User::setInvLastname); + binder.forField(invStreetField).bind(User::getInvStreet, User::setInvStreet); + binder.forField(invHouseNumberField).bind(User::getInvHouseNumber, User::setInvHouseNumber); + binder.forField(invAddressAddField).bind(User::getInvAddressAddition, User::setInvAddressAddition); + binder.forField(invZipField).bind(User::getInvZip, User::setInvZip); + binder.forField(invCityField).bind(User::getInvCity, User::setInvCity); // Pflichtindikator sichtbar machen firstnameField.setRequiredIndicatorVisible(true); lastnameField.setRequiredIndicatorVisible(true); @@ -232,7 +250,7 @@ public class EditProfileView extends HorizontalLayout { partsTitle.getStyle().set("margin", "0 0 var(--lumo-space-s) 0"); // Felder für Rechnungsstellung (für Live-Update) - Checkbox billingEnabled = new Checkbox("Rechnungslegung über votianLT"); + billingEnabled = new Checkbox("Rechnungslegung über votianLT"); billingEnabled.setValue(false); billingEnabled.addValueChangeListener(e -> toggleBilling(e.getValue())); @@ -302,6 +320,9 @@ public class EditProfileView extends HorizontalLayout { billingTab.add(billingLeft, billingRight); tabSheet.add("Rechnungsstellung", billingTab); + // Bestehende Rechnungsdaten laden (nach Erstellung aller Felder) + loadInvoiceData(); + // Zweiter Tab: Einstellungen (Beispiel mit Schaltern) VerticalLayout switches = new VerticalLayout(); switches.setPadding(false); @@ -342,9 +363,19 @@ public class EditProfileView extends HorizontalLayout { saveProfile.addClickListener(e -> { if (binder.validate().isOk()) { try { + // Check if billing status changed + boolean oldBillingStatus = currentInvoiceData != null && currentInvoiceData.isBillingEnabled(); + boolean newBillingStatus = billingEnabled.getValue(); + binder.writeBean(currentUser); userService.save(currentUser); + saveInvoiceData(); Notification.show("Profil gespeichert", 3000, Notification.Position.BOTTOM_END); + + // Always reload if billing status changed to update the sidebar + if (oldBillingStatus != newBillingStatus) { + UI.getCurrent().getPage().reload(); + } } catch (Exception ex) { Notification.show("Fehler beim Speichern: " + ex.getMessage(), 4000, Notification.Position.MIDDLE); } @@ -445,4 +476,45 @@ public class EditProfileView extends HorizontalLayout { return f != null && f.getValue() != null ? f.getValue() : ""; } + private void loadInvoiceData() { + currentInvoiceData = userInvoiceDataService.findByUserId(currentUser.getId()).orElse(null); + + if (currentInvoiceData != null) { + billingEnabled.setValue(currentInvoiceData.isBillingEnabled()); + prefixField.setValue(safe(currentInvoiceData.getPrefix())); + ustIdField.setValue(safe(currentInvoiceData.getUstId())); + taxNumberField.setValue(safe(currentInvoiceData.getTaxNumber())); + bankNameField.setValue(safe(currentInvoiceData.getBankName())); + ibanField.setValue(safe(currentInvoiceData.getIban())); + taxRateField.setValue(safe(currentInvoiceData.getTaxRate())); + introTextArea.setValue(safe(currentInvoiceData.getIntroText())); + termsTextArea.setValue(safe(currentInvoiceData.getPaymentTerms())); + + // Update field enabled state and PDF preview based on loaded state + setBillingFieldsEnabled(currentInvoiceData.isBillingEnabled()); + if (currentInvoiceData.isBillingEnabled()) { + refreshPdf(); + } + } + } + + private void saveInvoiceData() { + currentInvoiceData = userInvoiceDataService.createOrUpdate( + currentUser.getId(), + billingEnabled.getValue(), + prefixField.getValue(), + ustIdField.getValue(), + taxNumberField.getValue(), + bankNameField.getValue(), + ibanField.getValue(), + taxRateField.getValue(), + introTextArea.getValue(), + termsTextArea.getValue() + ); + } + + private String safe(String value) { + return value != null ? value : ""; + } + } diff --git a/src/main/java/de/assecutor/votianlt/repository/UserInvoiceDataRepository.java b/src/main/java/de/assecutor/votianlt/repository/UserInvoiceDataRepository.java new file mode 100644 index 0000000..39973c3 --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/repository/UserInvoiceDataRepository.java @@ -0,0 +1,16 @@ +package de.assecutor.votianlt.repository; + +import de.assecutor.votianlt.model.UserInvoiceData; +import org.bson.types.ObjectId; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UserInvoiceDataRepository extends MongoRepository { + + Optional findByUserId(ObjectId userId); + + void deleteByUserId(ObjectId userId); +} \ No newline at end of file