Speichern-Button, eindeutige Namen und E-Mail-Validierung
- "Eingaben speichern" neben "E-Rechnung erzeugen"; aktiv nur, wenn keine Vorbelegung geladen ist oder sich Eingaben seither geändert haben - Unique-Indizes auf die Namen von Templates und Vorbelegungen (werden nach der Verbindungsprüfung beim Start angelegt); kein Namensvorschlag mehr im Speichern-Dialog - E-Mail-Felder validieren sofort beim Verlassen des Feldes (Maske und Adressbuch, gemeinsames Pattern) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
package de.assecutor.pdftool.template;
|
||||
|
||||
import de.assecutor.pdftool.invoice.InvoiceDraft;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.boot.autoconfigure.mongo.MongoClientSettingsBuilderCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.mongodb.core.MongoTemplate;
|
||||
import org.springframework.data.mongodb.core.index.Index;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -15,6 +18,12 @@ import java.util.concurrent.TimeUnit;
|
||||
* Prüft beim Start die Verbindung zum MongoDB-Server und bricht den Start ab,
|
||||
* wenn er nicht erreichbar ist — die Anwendung setzt die Datenbank zwingend
|
||||
* voraus. Konfiguriert wird die Verbindung über MONGODB_URI in der .env-Datei.
|
||||
*
|
||||
* <p>Nach erfolgreicher Prüfung werden die Unique-Indizes auf die Template-
|
||||
* bzw. Vorbelegungs-Namen sichergestellt (Spring legt Indizes aus
|
||||
* {@code @Indexed} nicht automatisch an; auto-index-creation bleibt bewusst
|
||||
* aus, damit ein nicht erreichbarer Server zuerst diese klare Fehlermeldung
|
||||
* liefert).</p>
|
||||
*/
|
||||
@Component
|
||||
public class MongoConnectionCheck implements ApplicationRunner {
|
||||
@@ -38,6 +47,14 @@ public class MongoConnectionCheck implements ApplicationRunner {
|
||||
"Keine Verbindung zum MongoDB-Server. Die Anwendung benötigt die Datenbank zwingend — "
|
||||
+ "bitte MONGODB_URI in der .env-Datei prüfen. Ursache: " + e.getMessage(), e);
|
||||
}
|
||||
ensureUniqueNameIndexes();
|
||||
}
|
||||
|
||||
/** Stellt sicher, dass Template- und Vorbelegungs-Namen in der Datenbank eindeutig sind. */
|
||||
private void ensureUniqueNameIndexes() {
|
||||
Index uniqueName = new Index().on("name", Sort.Direction.ASC).unique();
|
||||
mongoTemplate.indexOps(InvoiceTemplate.class).createIndex(uniqueName);
|
||||
mongoTemplate.indexOps(InvoiceDraft.class).createIndex(uniqueName);
|
||||
}
|
||||
|
||||
/** Kurzer Auswahl-Timeout, damit ein nicht erreichbarer Server den Start schnell abbricht. */
|
||||
|
||||
@@ -36,6 +36,8 @@ import java.util.Optional;
|
||||
@PageTitle("Adressbuch")
|
||||
public class AddressBookView extends VerticalLayout {
|
||||
|
||||
private static final String EMAIL_PATTERN = "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$";
|
||||
|
||||
private final AddressRepository repository;
|
||||
|
||||
private final Grid<Address> grid = new Grid<>(Address.class, false);
|
||||
@@ -142,6 +144,13 @@ public class AddressBookView extends VerticalLayout {
|
||||
vatId.setHelperText("z. B. DE261094748");
|
||||
EmailField email = new EmailField("E-Mail");
|
||||
email.setValue(target.getEmail() != null ? target.getEmail() : "");
|
||||
// E-Mail direkt beim Verlassen des Feldes prüfen
|
||||
email.addValueChangeListener(event -> {
|
||||
String value = event.getValue();
|
||||
boolean valid = value == null || value.isBlank() || value.matches(EMAIL_PATTERN);
|
||||
email.setErrorMessage("Gültige E-Mail-Adresse angeben.");
|
||||
email.setInvalid(!valid);
|
||||
});
|
||||
TextField phone = new TextField("Telefon");
|
||||
phone.setValue(target.getPhone() != null ? target.getPhone() : "");
|
||||
|
||||
@@ -175,7 +184,7 @@ public class AddressBookView extends VerticalLayout {
|
||||
errors.add(vatError.get());
|
||||
}
|
||||
}
|
||||
if (!email.getValue().isBlank() && !email.getValue().matches("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$")) {
|
||||
if (!email.getValue().isBlank() && !email.getValue().matches(EMAIL_PATTERN)) {
|
||||
email.setErrorMessage("Gültige E-Mail-Adresse angeben.");
|
||||
email.setInvalid(true);
|
||||
errors.add("E-Mail-Adresse ist ungültig.");
|
||||
|
||||
@@ -87,7 +87,17 @@ public class MainView extends VerticalLayout {
|
||||
private final InvoiceDraftService invoiceDraftService;
|
||||
|
||||
/** Laden zuvor gespeicherter Eingaben dieser Maske. */
|
||||
private final ComboBox<String> draftSelect = new ComboBox<>("Gespeicherte Eingaben");
|
||||
private final ComboBox<String> draftSelect = new ComboBox<>("Vorbelegung laden");
|
||||
|
||||
private final Button saveDraftButton = new Button("Eingaben speichern", event -> openSaveDraftDialog());
|
||||
|
||||
/**
|
||||
* Formularstand zum Zeitpunkt des letzten Ladens bzw. Speicherns einer
|
||||
* Vorbelegung; {@code null}, solange keine geladen wurde. Der
|
||||
* Speichern-Button ist nur aktiv, wenn keine Vorbelegung geladen ist oder
|
||||
* sich die Eingaben seither geändert haben.
|
||||
*/
|
||||
private InvoiceDraft loadedDraftState;
|
||||
|
||||
private final Button generate = new Button("E-Rechnung erzeugen", event -> generate());
|
||||
|
||||
@@ -135,18 +145,19 @@ public class MainView extends VerticalLayout {
|
||||
+ "und als ZIP-Datei bereitgestellt."));
|
||||
|
||||
// Eingaben unter einem Namen speichern bzw. gespeicherte Eingaben laden.
|
||||
draftSelect.setPlaceholder("Eingaben laden");
|
||||
draftSelect.setPlaceholder("Vorbelegung laden");
|
||||
draftSelect.setClearButtonVisible(true);
|
||||
draftSelect.setWidth("300px");
|
||||
draftSelect.addValueChangeListener(event -> {
|
||||
if (event.isFromClient() && event.getValue() != null) {
|
||||
loadDraft(event.getValue());
|
||||
} else if (event.isFromClient()) {
|
||||
// Auswahl entfernt: es gilt wieder "keine Vorbelegung geladen".
|
||||
loadedDraftState = null;
|
||||
updateSaveDraftEnabled();
|
||||
}
|
||||
});
|
||||
Button saveDraft = new Button("Eingaben speichern", event -> openSaveDraftDialog());
|
||||
HorizontalLayout draftBar = new HorizontalLayout(draftSelect, saveDraft);
|
||||
draftBar.setAlignItems(Alignment.END);
|
||||
add(draftBar);
|
||||
add(draftSelect);
|
||||
|
||||
Upload upload = new Upload(uploadBuffer);
|
||||
upload.setAcceptedFileTypes("application/pdf", ".pdf");
|
||||
@@ -226,7 +237,10 @@ public class MainView extends VerticalLayout {
|
||||
downloadLink.getElement().setAttribute("download", true);
|
||||
downloadLink.setVisible(false);
|
||||
|
||||
add(new HorizontalLayout(generate, downloadLink));
|
||||
add(new HorizontalLayout(generate, saveDraftButton, downloadLink));
|
||||
|
||||
registerDraftDirtyTracking();
|
||||
updateSaveDraftEnabled();
|
||||
|
||||
if (environment.acceptsProfiles(Profiles.of("dev"))) {
|
||||
prefillDevData();
|
||||
@@ -249,8 +263,6 @@ public class MainView extends VerticalLayout {
|
||||
dialog.setHeaderTitle("Eingaben speichern");
|
||||
|
||||
TextField nameField = new TextField("Name");
|
||||
nameField.setValue(draftSelect.getValue() != null ? draftSelect.getValue()
|
||||
: invoiceNumber.getValue().trim());
|
||||
nameField.setWidth("300px");
|
||||
dialog.add(nameField);
|
||||
|
||||
@@ -291,9 +303,17 @@ public class MainView extends VerticalLayout {
|
||||
}
|
||||
|
||||
private void saveDraft(String name) {
|
||||
try {
|
||||
invoiceDraftService.save(buildDraft(name));
|
||||
} catch (Exception ex) {
|
||||
showErrors(List.of("Die Eingaben konnten nicht unter \"" + name
|
||||
+ "\" gespeichert werden: " + ex.getMessage()));
|
||||
return;
|
||||
}
|
||||
draftSelect.setItems(invoiceDraftService.names());
|
||||
draftSelect.setValue(name);
|
||||
loadedDraftState = comparableState();
|
||||
updateSaveDraftEnabled();
|
||||
Notification.show("Eingaben \"" + name + "\" gespeichert.", 3000, Notification.Position.BOTTOM_START)
|
||||
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
|
||||
}
|
||||
@@ -350,6 +370,9 @@ public class MainView extends VerticalLayout {
|
||||
}
|
||||
refreshItemsGrid();
|
||||
updateControlsEnabled();
|
||||
// Frisch geladen: Speichern erst wieder anbieten, wenn sich etwas ändert.
|
||||
loadedDraftState = comparableState();
|
||||
updateSaveDraftEnabled();
|
||||
}
|
||||
|
||||
private void applyParty(PartyForm form, InvoiceMetadata.Party party) {
|
||||
@@ -438,6 +461,47 @@ public class MainView extends VerticalLayout {
|
||||
|
||||
private void refreshItemsGrid() {
|
||||
itemsGrid.setItems(List.copyOf(lineItems));
|
||||
updateSaveDraftEnabled();
|
||||
}
|
||||
|
||||
/** Aktualisiert den Speichern-Button bei jeder Änderung an den Eingabefeldern. */
|
||||
private void registerDraftDirtyTracking() {
|
||||
invoiceNumber.addValueChangeListener(event -> updateSaveDraftEnabled());
|
||||
issueDate.addValueChangeListener(event -> updateSaveDraftEnabled());
|
||||
deliveryDate.addValueChangeListener(event -> updateSaveDraftEnabled());
|
||||
dueDate.addValueChangeListener(event -> updateSaveDraftEnabled());
|
||||
currency.addValueChangeListener(event -> updateSaveDraftEnabled());
|
||||
paymentTerms.addValueChangeListener(event -> updateSaveDraftEnabled());
|
||||
buyerReference.addValueChangeListener(event -> updateSaveDraftEnabled());
|
||||
iban.addValueChangeListener(event -> updateSaveDraftEnabled());
|
||||
bic.addValueChangeListener(event -> updateSaveDraftEnabled());
|
||||
templateSelect.addValueChangeListener(event -> updateSaveDraftEnabled());
|
||||
sender.onAnyChange(this::updateSaveDraftEnabled);
|
||||
recipient.onAnyChange(this::updateSaveDraftEnabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Der Speichern-Button ist nur aktiv, wenn keine Vorbelegung geladen wurde
|
||||
* oder sich die Eingaben seit dem Laden bzw. Speichern geändert haben.
|
||||
*/
|
||||
private void updateSaveDraftEnabled() {
|
||||
saveDraftButton.setEnabled(loadedDraftState == null || !comparableState().equals(loadedDraftState));
|
||||
}
|
||||
|
||||
/** Der aktuelle Formularstand ohne Name und Zeitstempel, zum Vergleich mit der geladenen Vorbelegung. */
|
||||
private InvoiceDraft comparableState() {
|
||||
return new InvoiceDraft(null, "",
|
||||
templateSelect.getValue(),
|
||||
invoiceNumber.getValue().trim(),
|
||||
issueDate.getValue(), deliveryDate.getValue(), dueDate.getValue(),
|
||||
currency.getValue().trim(),
|
||||
paymentTerms.getValue(),
|
||||
buyerReference.getValue().trim(),
|
||||
iban.getValue().trim(),
|
||||
bic.getValue().trim(),
|
||||
sender.toParty(), recipient.toParty(),
|
||||
List.copyOf(lineItems),
|
||||
null);
|
||||
}
|
||||
|
||||
/** Dialog zum Anlegen ({@code existing == null}) bzw. Bearbeiten einer Rechnungsposition. */
|
||||
@@ -729,6 +793,8 @@ public class MainView extends VerticalLayout {
|
||||
*/
|
||||
private static class PartyForm extends FormLayout {
|
||||
|
||||
static final String EMAIL_PATTERN = "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$";
|
||||
|
||||
private final boolean vatRequired;
|
||||
private final TextField name = new TextField("Firma / Name");
|
||||
private final TextField street = new TextField("Straße und Hausnummer");
|
||||
@@ -757,6 +823,8 @@ public class MainView extends VerticalLayout {
|
||||
// Elektronische Adresse (BT-34/BT-49) ist nach PEPPOL-EN16931 Pflicht
|
||||
email.setRequiredIndicatorVisible(true);
|
||||
email.setErrorMessage("Gültige E-Mail-Adresse angeben.");
|
||||
// E-Mail direkt beim Verlassen des Feldes prüfen
|
||||
email.addValueChangeListener(event -> checkEmail());
|
||||
if (vatRequired) {
|
||||
phone.setHelperText("Für den Verkäufer-Kontakt (BR-DE-6) empfohlen");
|
||||
}
|
||||
@@ -776,6 +844,18 @@ public class MainView extends VerticalLayout {
|
||||
phone.setValue(phoneValue);
|
||||
}
|
||||
|
||||
/** Meldet jede Wertänderung eines Feldes, z. B. für die Dirty-Erkennung. */
|
||||
void onAnyChange(Runnable listener) {
|
||||
name.addValueChangeListener(event -> listener.run());
|
||||
street.addValueChangeListener(event -> listener.run());
|
||||
zip.addValueChangeListener(event -> listener.run());
|
||||
city.addValueChangeListener(event -> listener.run());
|
||||
countryCode.addValueChangeListener(event -> listener.run());
|
||||
vatId.addValueChangeListener(event -> listener.run());
|
||||
email.addValueChangeListener(event -> listener.run());
|
||||
phone.addValueChangeListener(event -> listener.run());
|
||||
}
|
||||
|
||||
/** Leert alle Felder; das Land behält den Standardwert "DE". */
|
||||
void clearFields() {
|
||||
prefill("", "", "", "", "DE", "", "", "");
|
||||
@@ -788,6 +868,19 @@ public class MainView extends VerticalLayout {
|
||||
email.setInvalid(false);
|
||||
}
|
||||
|
||||
/** @return true, wenn die E-Mail-Adresse gültig (oder leer) ist. */
|
||||
private boolean checkEmail() {
|
||||
String value = email.getValue();
|
||||
if (value == null || value.isBlank()) {
|
||||
email.setInvalid(false);
|
||||
return true;
|
||||
}
|
||||
boolean valid = value.matches(EMAIL_PATTERN);
|
||||
email.setErrorMessage("Gültige E-Mail-Adresse angeben.");
|
||||
email.setInvalid(!valid);
|
||||
return valid;
|
||||
}
|
||||
|
||||
/** @return true, wenn die USt-IdNr. gültig (oder leer) ist. */
|
||||
private boolean checkVatId() {
|
||||
String value = vatId.getValue();
|
||||
@@ -820,12 +913,8 @@ public class MainView extends VerticalLayout {
|
||||
email.setErrorMessage(partyLabel + ": E-Mail ist ein Pflichtfeld.");
|
||||
email.setInvalid(true);
|
||||
errors.add(partyLabel + ": E-Mail ist ein Pflichtfeld (elektronische Adresse der E-Rechnung).");
|
||||
} else if (!email.getValue().matches("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$")) {
|
||||
email.setErrorMessage("Gültige E-Mail-Adresse angeben.");
|
||||
email.setInvalid(true);
|
||||
} else if (!checkEmail()) {
|
||||
errors.add(partyLabel + ": E-Mail-Adresse ist ungültig.");
|
||||
} else {
|
||||
email.setInvalid(false);
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user