Rechnungsmaske: ZIP-Auto-Download, löschbare Vorbelegungen, Icon- und Textausrichtungs-Korrekturen
- Valides ZIP startet den Download direkt; der sichtbare Download-Link entfällt - Vorbelegung laden: Löschen-Icon rechts an jedem Eintrag, Löschung aus MongoDB nach Sicherheitsabfrage - Template-Wechsel leert die Rechnungssteller-Felder nicht mehr, damit "Eingaben speichern" aktiv wird - Aktionsspalten der Grids mit fester Breite, damit die Icons nicht abgeschnitten werden - Texte im PDF-Renderer und Designer-Canvas oben statt zentriert ausgerichtet, damit mehrzeilige Variablenwerte auf Höhe ihrer Nachbar-Elemente beginnen - Vaadin-Frontend-Scaffolding (package.json, tsconfig, vite-Konfiguration) aufgenommen Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,11 @@ public class InvoiceDraftService {
|
||||
return repository.existsByName(name);
|
||||
}
|
||||
|
||||
/** Löscht die unter dem Namen gespeicherten Eingaben; ohne Treffer passiert nichts. */
|
||||
public void delete(String name) {
|
||||
repository.findByName(name).ifPresent(existing -> repository.deleteById(existing.id()));
|
||||
}
|
||||
|
||||
/** @return die Namen aller gespeicherten Eingaben, alphabetisch sortiert. */
|
||||
public List<String> names() {
|
||||
return repository.findAll().stream()
|
||||
|
||||
@@ -165,7 +165,10 @@ public class TemplatePdfService {
|
||||
if ("services.list".equals(variable)) {
|
||||
htmlBuilder.append("display:block;overflow:visible;padding:0;");
|
||||
} else {
|
||||
htmlBuilder.append("display:flex;align-items:center;");
|
||||
// Oben ausrichten statt zentrieren: Die erste Zeile muss an der
|
||||
// Box-Oberkante beginnen, sonst verschiebt sich mehrzeiliger
|
||||
// Variablentext gegenüber einzeiligen Nachbar-Elementen.
|
||||
htmlBuilder.append("display:flex;align-items:flex-start;");
|
||||
switch (textAlign) {
|
||||
case "center":
|
||||
htmlBuilder.append("justify-content:center;text-align:center;");
|
||||
|
||||
@@ -85,6 +85,8 @@ public class AddressBookView extends VerticalLayout {
|
||||
.setAutoWidth(true);
|
||||
grid.addColumn(address -> address.getName()).setHeader("Firma / Name").setSortable(true).setAutoWidth(true);
|
||||
grid.addColumn(address -> address.getStreet()).setHeader("Straße").setAutoWidth(true);
|
||||
// Feste Breite statt AutoWidth: Vaadin misst Komponentenspalten vor dem
|
||||
// Rendern der Buttons, dadurch würden die Icons rechts abgeschnitten.
|
||||
grid.addComponentColumn(address -> {
|
||||
Button edit = new Button(new Icon(VaadinIcon.EDIT), event -> openEditDialog(address));
|
||||
edit.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_SMALL);
|
||||
@@ -93,8 +95,11 @@ public class AddressBookView extends VerticalLayout {
|
||||
delete.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_ERROR,
|
||||
ButtonVariant.LUMO_SMALL);
|
||||
delete.setTooltipText("Löschen");
|
||||
return new HorizontalLayout(edit, delete);
|
||||
}).setHeader("").setFlexGrow(0).setAutoWidth(true);
|
||||
HorizontalLayout actions = new HorizontalLayout(edit, delete);
|
||||
actions.setSpacing(false);
|
||||
actions.setPadding(false);
|
||||
return actions;
|
||||
}).setHeader("").setFlexGrow(0).setWidth("110px");
|
||||
grid.addItemDoubleClickListener(event -> openEditDialog(event.getItem()));
|
||||
grid.setSizeFull();
|
||||
add(grid);
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.vaadin.flow.component.html.Anchor;
|
||||
import com.vaadin.flow.component.html.Div;
|
||||
import com.vaadin.flow.component.html.H3;
|
||||
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;
|
||||
@@ -24,6 +25,7 @@ import com.vaadin.flow.component.textfield.EmailField;
|
||||
import com.vaadin.flow.component.textfield.TextField;
|
||||
import com.vaadin.flow.component.upload.Upload;
|
||||
import com.vaadin.flow.component.upload.receivers.MemoryBuffer;
|
||||
import com.vaadin.flow.data.renderer.ComponentRenderer;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import com.vaadin.flow.server.StreamResource;
|
||||
@@ -143,6 +145,25 @@ public class MainView extends VerticalLayout {
|
||||
draftSelect.setPlaceholder("Vorbelegung laden");
|
||||
draftSelect.setClearButtonVisible(true);
|
||||
draftSelect.setWidth("300px");
|
||||
// Jeder Eintrag zeigt rechts ein Löschen-Icon, das die Vorbelegung
|
||||
// nach einer Sicherheitsabfrage aus MongoDB entfernt.
|
||||
draftSelect.setRenderer(new ComponentRenderer<>(name -> {
|
||||
Span label = new Span(name);
|
||||
Icon delete = VaadinIcon.TRASH.create();
|
||||
delete.setSize("var(--lumo-icon-size-s)");
|
||||
delete.getStyle().set("color", "var(--lumo-error-color)").set("cursor", "pointer");
|
||||
delete.getElement().setAttribute("title", "Vorbelegung löschen");
|
||||
// Der Klick auf das Icon darf den Eintrag nicht auswählen; der
|
||||
// Server-Listener auf dem Icon selbst wird davon nicht berührt.
|
||||
delete.getElement().executeJs("this.addEventListener('click', function(e) { e.stopPropagation(); })");
|
||||
delete.getElement().addEventListener("click", e -> confirmDeleteDraft(name));
|
||||
HorizontalLayout row = new HorizontalLayout(label, delete);
|
||||
row.setJustifyContentMode(JustifyContentMode.BETWEEN);
|
||||
row.setAlignItems(Alignment.CENTER);
|
||||
row.setWidthFull();
|
||||
row.setPadding(false);
|
||||
return row;
|
||||
}));
|
||||
draftSelect.addValueChangeListener(event -> {
|
||||
if (event.isFromClient() && event.getValue() != null) {
|
||||
loadDraft(event.getValue());
|
||||
@@ -181,9 +202,14 @@ public class MainView extends VerticalLayout {
|
||||
// Entweder PDF oder Template: Auswahl entfernt ein hochgeladenes PDF.
|
||||
upload.clearFileList();
|
||||
uploadedPdf = null;
|
||||
// Rechnungssteller-Felder leer beginnen lassen.
|
||||
senderAddressSelect.clear();
|
||||
sender.clearFields();
|
||||
// Rechnungssteller-Felder nur bei der ersten Template-Auswahl
|
||||
// leer beginnen lassen; beim Wechsel zwischen Templates bleiben
|
||||
// die Eingaben erhalten (sonst wäre u. a. der Speichern-Button
|
||||
// wegen leerer Pflichtfelder nie aktiv).
|
||||
if (event.getOldValue() == null) {
|
||||
senderAddressSelect.clear();
|
||||
sender.clearFields();
|
||||
}
|
||||
}
|
||||
updateControlsEnabled();
|
||||
});
|
||||
@@ -226,8 +252,11 @@ public class MainView extends VerticalLayout {
|
||||
recipientAddressSelect, recipient, itemsGrid, addItem, generate));
|
||||
updateControlsEnabled();
|
||||
|
||||
// Unsichtbarer Anchor: dient nur als Träger für den automatischen
|
||||
// Download nach dem Erzeugen (display:none statt setVisible, damit
|
||||
// callJsFunction("click") den Klick sofort ausführen kann).
|
||||
downloadLink.getElement().setAttribute("download", true);
|
||||
downloadLink.setVisible(false);
|
||||
downloadLink.getStyle().set("display", "none");
|
||||
|
||||
add(new HorizontalLayout(generate, saveDraftButton, downloadLink));
|
||||
|
||||
@@ -290,6 +319,39 @@ public class MainView extends VerticalLayout {
|
||||
confirm.open();
|
||||
}
|
||||
|
||||
/** Sicherheitsabfrage vor dem endgültigen Löschen einer Vorbelegung. */
|
||||
private void confirmDeleteDraft(String name) {
|
||||
ConfirmDialog confirm = new ConfirmDialog();
|
||||
confirm.setHeader("Vorbelegung löschen?");
|
||||
confirm.setText("Sollen die gespeicherten Eingaben \"" + name + "\" endgültig gelöscht werden?");
|
||||
confirm.setConfirmText("Löschen");
|
||||
confirm.setConfirmButtonTheme("error primary");
|
||||
confirm.setCancelable(true);
|
||||
confirm.setCancelText("Abbrechen");
|
||||
confirm.addConfirmListener(event -> deleteDraft(name));
|
||||
confirm.open();
|
||||
}
|
||||
|
||||
private void deleteDraft(String name) {
|
||||
try {
|
||||
invoiceDraftService.delete(name);
|
||||
} catch (Exception ex) {
|
||||
showErrors(List.of("Die Vorbelegung \"" + name + "\" konnte nicht gelöscht werden: " + ex.getMessage()));
|
||||
return;
|
||||
}
|
||||
boolean wasSelected = name.equals(draftSelect.getValue());
|
||||
draftSelect.setItems(invoiceDraftService.names());
|
||||
if (wasSelected) {
|
||||
// Die geladene Vorbelegung existiert nicht mehr; das Formular
|
||||
// behält seine Werte, gilt aber wieder als ungespeichert.
|
||||
draftSelect.clear();
|
||||
loadedDraftState = null;
|
||||
updateSaveDraftEnabled();
|
||||
}
|
||||
Notification.show("Vorbelegung \"" + name + "\" gelöscht.", 3000, Notification.Position.BOTTOM_START)
|
||||
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
|
||||
}
|
||||
|
||||
private void saveDraft(String name) {
|
||||
try {
|
||||
invoiceDraftService.save(buildDraft(name));
|
||||
@@ -412,6 +474,8 @@ public class MainView extends VerticalLayout {
|
||||
.setTextAlign(ColumnTextAlign.END).setAutoWidth(true).setFlexGrow(0);
|
||||
itemsGrid.addColumn(item -> formatDecimal(item.vatPercent())).setHeader("USt %")
|
||||
.setTextAlign(ColumnTextAlign.END).setAutoWidth(true).setFlexGrow(0);
|
||||
// Feste Breite statt AutoWidth: Vaadin misst Komponentenspalten vor dem
|
||||
// Rendern der Buttons, dadurch würden die Icons rechts abgeschnitten.
|
||||
itemsGrid.addComponentColumn(item -> {
|
||||
Button edit = new Button(new Icon(VaadinIcon.EDIT), event -> openItemDialog(item));
|
||||
edit.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_SMALL);
|
||||
@@ -423,8 +487,11 @@ public class MainView extends VerticalLayout {
|
||||
delete.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_ERROR,
|
||||
ButtonVariant.LUMO_SMALL);
|
||||
delete.setTooltipText("Löschen");
|
||||
return new HorizontalLayout(edit, delete);
|
||||
}).setHeader("").setAutoWidth(true).setFlexGrow(0);
|
||||
HorizontalLayout actions = new HorizontalLayout(edit, delete);
|
||||
actions.setSpacing(false);
|
||||
actions.setPadding(false);
|
||||
return actions;
|
||||
}).setHeader("").setWidth("110px").setFlexGrow(0);
|
||||
itemsGrid.addItemDoubleClickListener(event -> openItemDialog(event.getItem()));
|
||||
itemsGrid.setAllRowsVisible(true);
|
||||
itemsGrid.setWidthFull();
|
||||
@@ -561,7 +628,6 @@ public class MainView extends VerticalLayout {
|
||||
}
|
||||
|
||||
private void generate() {
|
||||
downloadLink.setVisible(false);
|
||||
List<String> errors = validateForm();
|
||||
if (!errors.isEmpty()) {
|
||||
showErrors(errors);
|
||||
@@ -642,13 +708,12 @@ public class MainView extends VerticalLayout {
|
||||
() -> new ByteArrayInputStream(result.zip()));
|
||||
resource.setContentType("application/zip");
|
||||
downloadLink.setHref(resource);
|
||||
downloadLink.setText("ZIP herunterladen (" + result.zipFileName() + ")");
|
||||
downloadLink.setVisible(true);
|
||||
|
||||
if (result.valid()) {
|
||||
Notification.show("E-Rechnung erzeugt und erfolgreich validiert.", 4000,
|
||||
Notification.Position.BOTTOM_START)
|
||||
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
|
||||
downloadLink.getElement().callJsFunction("click");
|
||||
} else {
|
||||
Notification.show("E-Rechnung erzeugt, aber die Validierung meldet Probleme. "
|
||||
+ "Details stehen im Prüfbericht (validation-report.xml) im ZIP.", 8000,
|
||||
|
||||
Reference in New Issue
Block a user