Erweiterungen

This commit is contained in:
2025-09-23 10:28:19 +02:00
parent b430741b57
commit 1a5ea5d47a
6 changed files with 246 additions and 20 deletions

View File

@@ -23,12 +23,25 @@ public class User {
// Firmen-/Adressdaten // Firmen-/Adressdaten
private String company; // Firma private String company; // Firma
private String companyAddition; // Firmenzusatz
private String street; // Straße private String street; // Straße
private String houseNumber; // Hausnr private String houseNumber; // Hausnr
private String addressAddition; // Adresszusatz (optional) private String addressAddition; // Adresszusatz (optional)
private String zip; // Postleitzahl private String zip; // Postleitzahl
private String city; // Stadt 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) @Indexed(unique = true)
private String email; private String email;

View File

@@ -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();
}
}

View File

@@ -20,6 +20,9 @@ import com.vaadin.flow.router.Layout;
import com.vaadin.flow.server.auth.AnonymousAllowed; import com.vaadin.flow.server.auth.AnonymousAllowed;
import com.vaadin.flow.server.menu.MenuConfiguration; import com.vaadin.flow.server.menu.MenuConfiguration;
import com.vaadin.flow.server.menu.MenuEntry; 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.pages.view.EditProfileView;
import de.assecutor.votianlt.security.SecurityService; import de.assecutor.votianlt.security.SecurityService;
@@ -31,12 +34,14 @@ import static com.vaadin.flow.theme.lumo.LumoUtility.*;
public final class MainLayout extends AppLayout { public final class MainLayout extends AppLayout {
private final SecurityService securityService; private final SecurityService securityService;
private final UserInvoiceDataService userInvoiceDataService;
private Div headerRef; private Div headerRef;
private Scroller navRef; private Scroller navRef;
private Component userMenuRef; private Component userMenuRef;
public MainLayout(SecurityService securityService) { public MainLayout(SecurityService securityService, UserInvoiceDataService userInvoiceDataService) {
this.securityService = securityService; this.securityService = securityService;
this.userInvoiceDataService = userInvoiceDataService;
setPrimarySection(Section.DRAWER); setPrimarySection(Section.DRAWER);
// Always build the drawer; keep references and toggle visibility on attach and // 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 customers = new SideNavItem("Kunden", "customers", new Icon(VaadinIcon.USERS));
SideNavItem appUsers = new SideNavItem("App-Nutzer", "app-user", 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 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)); 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); verwaltungDetails.add(verwaltungContent);
// Create Details component for "Verwaltung" with collapsible list // Create Details component for "Verwaltung" with collapsible list
@@ -179,4 +191,18 @@ public final class MainLayout extends AppLayout {
return userMenu; 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;
}
} }

View File

@@ -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<UserInvoiceData> 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);
}
}

View File

@@ -13,6 +13,7 @@ import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.notification.Notification; import com.vaadin.flow.component.notification.Notification;
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;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.textfield.EmailField; import com.vaadin.flow.component.textfield.EmailField;
import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.component.textfield.TextField;
import java.io.ByteArrayOutputStream; 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.PageTitle;
import com.vaadin.flow.router.Route; import com.vaadin.flow.router.Route;
import de.assecutor.votianlt.model.User; 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.UserService;
import de.assecutor.votianlt.pages.service.UserInvoiceDataService;
import de.assecutor.votianlt.security.SecurityService; import de.assecutor.votianlt.security.SecurityService;
import jakarta.annotation.security.RolesAllowed; import jakarta.annotation.security.RolesAllowed;
@@ -46,8 +49,12 @@ public class EditProfileView extends HorizontalLayout {
private final Binder<User> binder = new Binder<>(User.class); private final Binder<User> binder = new Binder<>(User.class);
private final User currentUser; 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(); this.currentUser = securityService.getCurrentDatabaseUser();
setSizeFull(); setSizeFull();
setPadding(true); setPadding(true);
@@ -155,32 +162,43 @@ public class EditProfileView extends HorizontalLayout {
form.add(mobileField, 2); form.add(mobileField, 2);
form.add(emailField, 2); form.add(emailField, 2);
// Binder: Pflichtfelder und Bindings // Binder: Pflichtfelder und Bindings
// UI-Pflichtfelder ohne Datenbindung (da nicht im User-Modell vorhanden) // Pflichtfelder markieren
companyField.setRequiredIndicatorVisible(true); companyField.setRequiredIndicatorVisible(true);
streetField.setRequiredIndicatorVisible(true); streetField.setRequiredIndicatorVisible(true);
houseNumberField.setRequiredIndicatorVisible(true); houseNumberField.setRequiredIndicatorVisible(true);
zipField.setRequiredIndicatorVisible(true); zipField.setRequiredIndicatorVisible(true);
cityField.setRequiredIndicatorVisible(true); cityField.setRequiredIndicatorVisible(true);
binder.forField(companyField).asRequired("").bind(user -> null, (user, v) -> { // Hauptadresse binden
}); binder.forField(companyField).asRequired("Firma ist erforderlich").bind(User::getCompany, User::setCompany);
binder.forField(streetField).asRequired("").bind(user -> null, (user, v) -> { 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("").bind(user -> null, (user, v) -> { binder.forField(houseNumberField).asRequired("Hausnummer ist erforderlich").bind(User::getHouseNumber, User::setHouseNumber);
}); binder.forField(addressAddField).bind(User::getAddressAddition, User::setAddressAddition);
binder.forField(zipField).asRequired("").bind(user -> null, (user, v) -> { 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(cityField).asRequired("").bind(user -> null, (user, v) -> {
});
binder.forField(firstnameField).asRequired("").bind(User::getFirstname, User::setFirstname); // Personendaten binden
binder.forField(lastnameField).asRequired("").bind(User::getName, User::setName); binder.forField(firstnameField).asRequired("Vorname ist erforderlich").bind(User::getFirstname, User::setFirstname);
binder.forField(phoneField).asRequired("").bind(User::getPhone, User::setPhone); binder.forField(lastnameField).asRequired("Nachname ist erforderlich").bind(User::getName, User::setName);
binder.forField(emailField).asRequired("").withValidator(new EmailValidator("Ungültige E-Mail-Adresse")) 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); .bind(User::getEmail, User::setEmail);
// Optionale Felder // Optionale Felder
binder.forField(mobileField).bind(User::getPhone2, User::setPhone2); binder.forField(mobileField).bind(User::getPhone2, User::setPhone2);
binder.forField(faxField).bind(User::getFax, User::setFax); 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 // Pflichtindikator sichtbar machen
firstnameField.setRequiredIndicatorVisible(true); firstnameField.setRequiredIndicatorVisible(true);
lastnameField.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"); partsTitle.getStyle().set("margin", "0 0 var(--lumo-space-s) 0");
// Felder für Rechnungsstellung (für Live-Update) // 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.setValue(false);
billingEnabled.addValueChangeListener(e -> toggleBilling(e.getValue())); billingEnabled.addValueChangeListener(e -> toggleBilling(e.getValue()));
@@ -302,6 +320,9 @@ public class EditProfileView extends HorizontalLayout {
billingTab.add(billingLeft, billingRight); billingTab.add(billingLeft, billingRight);
tabSheet.add("Rechnungsstellung", billingTab); tabSheet.add("Rechnungsstellung", billingTab);
// Bestehende Rechnungsdaten laden (nach Erstellung aller Felder)
loadInvoiceData();
// Zweiter Tab: Einstellungen (Beispiel mit Schaltern) // Zweiter Tab: Einstellungen (Beispiel mit Schaltern)
VerticalLayout switches = new VerticalLayout(); VerticalLayout switches = new VerticalLayout();
switches.setPadding(false); switches.setPadding(false);
@@ -342,9 +363,19 @@ public class EditProfileView extends HorizontalLayout {
saveProfile.addClickListener(e -> { saveProfile.addClickListener(e -> {
if (binder.validate().isOk()) { if (binder.validate().isOk()) {
try { try {
// Check if billing status changed
boolean oldBillingStatus = currentInvoiceData != null && currentInvoiceData.isBillingEnabled();
boolean newBillingStatus = billingEnabled.getValue();
binder.writeBean(currentUser); binder.writeBean(currentUser);
userService.save(currentUser); userService.save(currentUser);
saveInvoiceData();
Notification.show("Profil gespeichert", 3000, Notification.Position.BOTTOM_END); 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) { } catch (Exception ex) {
Notification.show("Fehler beim Speichern: " + ex.getMessage(), 4000, Notification.Position.MIDDLE); 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() : ""; 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 : "";
}
} }

View File

@@ -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<UserInvoiceData, ObjectId> {
Optional<UserInvoiceData> findByUserId(ObjectId userId);
void deleteByUserId(ObjectId userId);
}