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;
|
package de.assecutor.pdftool.template;
|
||||||
|
|
||||||
|
import de.assecutor.pdftool.invoice.InvoiceDraft;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.boot.ApplicationArguments;
|
import org.springframework.boot.ApplicationArguments;
|
||||||
import org.springframework.boot.ApplicationRunner;
|
import org.springframework.boot.ApplicationRunner;
|
||||||
import org.springframework.boot.autoconfigure.mongo.MongoClientSettingsBuilderCustomizer;
|
import org.springframework.boot.autoconfigure.mongo.MongoClientSettingsBuilderCustomizer;
|
||||||
import org.springframework.context.annotation.Bean;
|
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.MongoTemplate;
|
||||||
|
import org.springframework.data.mongodb.core.index.Index;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
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,
|
* 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
|
* wenn er nicht erreichbar ist — die Anwendung setzt die Datenbank zwingend
|
||||||
* voraus. Konfiguriert wird die Verbindung über MONGODB_URI in der .env-Datei.
|
* 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
|
@Component
|
||||||
public class MongoConnectionCheck implements ApplicationRunner {
|
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 — "
|
"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);
|
+ "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. */
|
/** Kurzer Auswahl-Timeout, damit ein nicht erreichbarer Server den Start schnell abbricht. */
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ import java.util.Optional;
|
|||||||
@PageTitle("Adressbuch")
|
@PageTitle("Adressbuch")
|
||||||
public class AddressBookView extends VerticalLayout {
|
public class AddressBookView extends VerticalLayout {
|
||||||
|
|
||||||
|
private static final String EMAIL_PATTERN = "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$";
|
||||||
|
|
||||||
private final AddressRepository repository;
|
private final AddressRepository repository;
|
||||||
|
|
||||||
private final Grid<Address> grid = new Grid<>(Address.class, false);
|
private final Grid<Address> grid = new Grid<>(Address.class, false);
|
||||||
@@ -142,6 +144,13 @@ public class AddressBookView extends VerticalLayout {
|
|||||||
vatId.setHelperText("z. B. DE261094748");
|
vatId.setHelperText("z. B. DE261094748");
|
||||||
EmailField email = new EmailField("E-Mail");
|
EmailField email = new EmailField("E-Mail");
|
||||||
email.setValue(target.getEmail() != null ? target.getEmail() : "");
|
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");
|
TextField phone = new TextField("Telefon");
|
||||||
phone.setValue(target.getPhone() != null ? target.getPhone() : "");
|
phone.setValue(target.getPhone() != null ? target.getPhone() : "");
|
||||||
|
|
||||||
@@ -175,7 +184,7 @@ public class AddressBookView extends VerticalLayout {
|
|||||||
errors.add(vatError.get());
|
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.setErrorMessage("Gültige E-Mail-Adresse angeben.");
|
||||||
email.setInvalid(true);
|
email.setInvalid(true);
|
||||||
errors.add("E-Mail-Adresse ist ungültig.");
|
errors.add("E-Mail-Adresse ist ungültig.");
|
||||||
|
|||||||
@@ -87,7 +87,17 @@ public class MainView extends VerticalLayout {
|
|||||||
private final InvoiceDraftService invoiceDraftService;
|
private final InvoiceDraftService invoiceDraftService;
|
||||||
|
|
||||||
/** Laden zuvor gespeicherter Eingaben dieser Maske. */
|
/** 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());
|
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."));
|
+ "und als ZIP-Datei bereitgestellt."));
|
||||||
|
|
||||||
// Eingaben unter einem Namen speichern bzw. gespeicherte Eingaben laden.
|
// Eingaben unter einem Namen speichern bzw. gespeicherte Eingaben laden.
|
||||||
draftSelect.setPlaceholder("Eingaben laden");
|
draftSelect.setPlaceholder("Vorbelegung laden");
|
||||||
draftSelect.setClearButtonVisible(true);
|
draftSelect.setClearButtonVisible(true);
|
||||||
draftSelect.setWidth("300px");
|
draftSelect.setWidth("300px");
|
||||||
draftSelect.addValueChangeListener(event -> {
|
draftSelect.addValueChangeListener(event -> {
|
||||||
if (event.isFromClient() && event.getValue() != null) {
|
if (event.isFromClient() && event.getValue() != null) {
|
||||||
loadDraft(event.getValue());
|
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());
|
add(draftSelect);
|
||||||
HorizontalLayout draftBar = new HorizontalLayout(draftSelect, saveDraft);
|
|
||||||
draftBar.setAlignItems(Alignment.END);
|
|
||||||
add(draftBar);
|
|
||||||
|
|
||||||
Upload upload = new Upload(uploadBuffer);
|
Upload upload = new Upload(uploadBuffer);
|
||||||
upload.setAcceptedFileTypes("application/pdf", ".pdf");
|
upload.setAcceptedFileTypes("application/pdf", ".pdf");
|
||||||
@@ -226,7 +237,10 @@ public class MainView extends VerticalLayout {
|
|||||||
downloadLink.getElement().setAttribute("download", true);
|
downloadLink.getElement().setAttribute("download", true);
|
||||||
downloadLink.setVisible(false);
|
downloadLink.setVisible(false);
|
||||||
|
|
||||||
add(new HorizontalLayout(generate, downloadLink));
|
add(new HorizontalLayout(generate, saveDraftButton, downloadLink));
|
||||||
|
|
||||||
|
registerDraftDirtyTracking();
|
||||||
|
updateSaveDraftEnabled();
|
||||||
|
|
||||||
if (environment.acceptsProfiles(Profiles.of("dev"))) {
|
if (environment.acceptsProfiles(Profiles.of("dev"))) {
|
||||||
prefillDevData();
|
prefillDevData();
|
||||||
@@ -249,8 +263,6 @@ public class MainView extends VerticalLayout {
|
|||||||
dialog.setHeaderTitle("Eingaben speichern");
|
dialog.setHeaderTitle("Eingaben speichern");
|
||||||
|
|
||||||
TextField nameField = new TextField("Name");
|
TextField nameField = new TextField("Name");
|
||||||
nameField.setValue(draftSelect.getValue() != null ? draftSelect.getValue()
|
|
||||||
: invoiceNumber.getValue().trim());
|
|
||||||
nameField.setWidth("300px");
|
nameField.setWidth("300px");
|
||||||
dialog.add(nameField);
|
dialog.add(nameField);
|
||||||
|
|
||||||
@@ -291,9 +303,17 @@ public class MainView extends VerticalLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void saveDraft(String name) {
|
private void saveDraft(String name) {
|
||||||
invoiceDraftService.save(buildDraft(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.setItems(invoiceDraftService.names());
|
||||||
draftSelect.setValue(name);
|
draftSelect.setValue(name);
|
||||||
|
loadedDraftState = comparableState();
|
||||||
|
updateSaveDraftEnabled();
|
||||||
Notification.show("Eingaben \"" + name + "\" gespeichert.", 3000, Notification.Position.BOTTOM_START)
|
Notification.show("Eingaben \"" + name + "\" gespeichert.", 3000, Notification.Position.BOTTOM_START)
|
||||||
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
|
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
|
||||||
}
|
}
|
||||||
@@ -350,6 +370,9 @@ public class MainView extends VerticalLayout {
|
|||||||
}
|
}
|
||||||
refreshItemsGrid();
|
refreshItemsGrid();
|
||||||
updateControlsEnabled();
|
updateControlsEnabled();
|
||||||
|
// Frisch geladen: Speichern erst wieder anbieten, wenn sich etwas ändert.
|
||||||
|
loadedDraftState = comparableState();
|
||||||
|
updateSaveDraftEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyParty(PartyForm form, InvoiceMetadata.Party party) {
|
private void applyParty(PartyForm form, InvoiceMetadata.Party party) {
|
||||||
@@ -438,6 +461,47 @@ public class MainView extends VerticalLayout {
|
|||||||
|
|
||||||
private void refreshItemsGrid() {
|
private void refreshItemsGrid() {
|
||||||
itemsGrid.setItems(List.copyOf(lineItems));
|
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. */
|
/** 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 {
|
private static class PartyForm extends FormLayout {
|
||||||
|
|
||||||
|
static final String EMAIL_PATTERN = "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$";
|
||||||
|
|
||||||
private final boolean vatRequired;
|
private final boolean vatRequired;
|
||||||
private final TextField name = new TextField("Firma / Name");
|
private final TextField name = new TextField("Firma / Name");
|
||||||
private final TextField street = new TextField("Straße und Hausnummer");
|
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
|
// Elektronische Adresse (BT-34/BT-49) ist nach PEPPOL-EN16931 Pflicht
|
||||||
email.setRequiredIndicatorVisible(true);
|
email.setRequiredIndicatorVisible(true);
|
||||||
email.setErrorMessage("Gültige E-Mail-Adresse angeben.");
|
email.setErrorMessage("Gültige E-Mail-Adresse angeben.");
|
||||||
|
// E-Mail direkt beim Verlassen des Feldes prüfen
|
||||||
|
email.addValueChangeListener(event -> checkEmail());
|
||||||
if (vatRequired) {
|
if (vatRequired) {
|
||||||
phone.setHelperText("Für den Verkäufer-Kontakt (BR-DE-6) empfohlen");
|
phone.setHelperText("Für den Verkäufer-Kontakt (BR-DE-6) empfohlen");
|
||||||
}
|
}
|
||||||
@@ -776,6 +844,18 @@ public class MainView extends VerticalLayout {
|
|||||||
phone.setValue(phoneValue);
|
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". */
|
/** Leert alle Felder; das Land behält den Standardwert "DE". */
|
||||||
void clearFields() {
|
void clearFields() {
|
||||||
prefill("", "", "", "", "DE", "", "", "");
|
prefill("", "", "", "", "DE", "", "", "");
|
||||||
@@ -788,6 +868,19 @@ public class MainView extends VerticalLayout {
|
|||||||
email.setInvalid(false);
|
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. */
|
/** @return true, wenn die USt-IdNr. gültig (oder leer) ist. */
|
||||||
private boolean checkVatId() {
|
private boolean checkVatId() {
|
||||||
String value = vatId.getValue();
|
String value = vatId.getValue();
|
||||||
@@ -820,12 +913,8 @@ public class MainView extends VerticalLayout {
|
|||||||
email.setErrorMessage(partyLabel + ": E-Mail ist ein Pflichtfeld.");
|
email.setErrorMessage(partyLabel + ": E-Mail ist ein Pflichtfeld.");
|
||||||
email.setInvalid(true);
|
email.setInvalid(true);
|
||||||
errors.add(partyLabel + ": E-Mail ist ein Pflichtfeld (elektronische Adresse der E-Rechnung).");
|
errors.add(partyLabel + ": E-Mail ist ein Pflichtfeld (elektronische Adresse der E-Rechnung).");
|
||||||
} else if (!email.getValue().matches("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$")) {
|
} else if (!checkEmail()) {
|
||||||
email.setErrorMessage("Gültige E-Mail-Adresse angeben.");
|
|
||||||
email.setInvalid(true);
|
|
||||||
errors.add(partyLabel + ": E-Mail-Adresse ist ungültig.");
|
errors.add(partyLabel + ": E-Mail-Adresse ist ungültig.");
|
||||||
} else {
|
|
||||||
email.setInvalid(false);
|
|
||||||
}
|
}
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user