diff --git a/pom.xml b/pom.xml index b31a52f..869cb80 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ de.assecutor.votianlt votianlt - 0.9.4 + 0.9.5 jar diff --git a/src/main/java/de/assecutor/votianlt/controller/MessageController.java b/src/main/java/de/assecutor/votianlt/controller/MessageController.java index b27638a..47cce95 100644 --- a/src/main/java/de/assecutor/votianlt/controller/MessageController.java +++ b/src/main/java/de/assecutor/votianlt/controller/MessageController.java @@ -380,7 +380,11 @@ public class MessageController { return; } - boolean allCompleted = allTasks.stream().allMatch(task -> task.isCompleted()); + var mandatoryTasks = allTasks.stream().filter(task -> !task.isOptional()).toList(); + if (mandatoryTasks.isEmpty()) { + return; + } + boolean allCompleted = mandatoryTasks.stream().allMatch(task -> task.isCompleted()); if (allCompleted) { updateJobStatusToCompleted(jobId); diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java b/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java index 555d355..64617f6 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java @@ -8,6 +8,7 @@ import com.vaadin.flow.component.confirmdialog.ConfirmDialog; import com.vaadin.flow.component.dialog.Dialog; import com.vaadin.flow.component.datepicker.DatePicker; +import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.H2; import com.vaadin.flow.component.timepicker.TimePicker; import com.vaadin.flow.component.html.H3; @@ -797,38 +798,42 @@ public class AddJobView extends Main implements HasDynamicTitle { summaryTitle.getStyle().set("margin", "0"); summaryLayout.add(summaryTitle); - // Net total - HorizontalLayout netRow = new HorizontalLayout(); - netRow.setWidthFull(); - netRow.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN); - Span netLabel = new Span(getTranslation("addjob.summary.net") + ":"); + Div priceTable = new Div(); + priceTable.getStyle().set("width", "100%"); + + // Net total row + Div netRow = new Div(); + netRow.getStyle().set("display", "flex").set("justify-content", "space-between").set("padding", "4px 0"); + Span netLabelSpan = new Span(getTranslation("addjob.summary.net") + ":"); + netLabelSpan.getStyle().set("padding-right", "8px"); netTotalLabel = new Span("0,00 €"); - netTotalLabel.getStyle().set("font-weight", "bold"); - netRow.add(netLabel, netTotalLabel); - summaryLayout.add(netRow); + netTotalLabel.getStyle().set("font-weight", "bold").set("white-space", "nowrap"); + netRow.add(netLabelSpan, netTotalLabel); + priceTable.add(netRow); - // VAT total - HorizontalLayout vatRow = new HorizontalLayout(); - vatRow.setWidthFull(); - vatRow.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN); - Span vatLabel = new Span(getTranslation("addjob.summary.vat") + ":"); + // VAT total row + Div vatRow = new Div(); + vatRow.getStyle().set("display", "flex").set("justify-content", "space-between").set("padding", "4px 0"); + Span vatLabelSpan = new Span(getTranslation("addjob.summary.vat") + ":"); + vatLabelSpan.getStyle().set("padding-right", "8px"); vatTotalLabel = new Span("0,00 €"); - vatTotalLabel.getStyle().set("font-weight", "bold"); - vatRow.add(vatLabel, vatTotalLabel); - summaryLayout.add(vatRow); + vatTotalLabel.getStyle().set("font-weight", "bold").set("white-space", "nowrap"); + vatRow.add(vatLabelSpan, vatTotalLabel); + priceTable.add(vatRow); - // Gross total - HorizontalLayout grossRow = new HorizontalLayout(); - grossRow.setWidthFull(); - grossRow.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN); - Span grossLabel = new Span(getTranslation("addjob.summary.gross") + ":"); - grossLabel.getStyle().set("font-size", "var(--lumo-font-size-l)"); + // Gross total row + Div grossRow = new Div(); + grossRow.getStyle().set("display", "flex").set("justify-content", "space-between").set("padding", "4px 0"); + Span grossLabelSpan = new Span(getTranslation("addjob.summary.gross") + ":"); + grossLabelSpan.getStyle().set("padding-right", "8px").set("font-weight", "bold"); grossTotalLabel = new Span("0,00 €"); grossTotalLabel.getStyle().set("font-size", "var(--lumo-font-size-l)"); grossTotalLabel.getStyle().set("font-weight", "bold"); - grossTotalLabel.getStyle().set("color", "var(--lumo-primary-text-color)"); - grossRow.add(grossLabel, grossTotalLabel); - summaryLayout.add(grossRow); + grossTotalLabel.getStyle().set("color", "var(--lumo-primary-text-color)").set("white-space", "nowrap"); + grossRow.add(grossLabelSpan, grossTotalLabel); + priceTable.add(grossRow); + + summaryLayout.add(priceTable); content.add(summaryLayout); diff --git a/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java b/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java index 0087c41..8dcb797 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java @@ -316,24 +316,40 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter BigDecimal vatAmount = netAmount.multiply(vatRate); BigDecimal totalAmount = netAmount.add(vatAmount); - VerticalLayout summaryInfo = new VerticalLayout(); - summaryInfo.setSpacing(true); - summaryInfo.setWidthFull(); + Div priceTable = new Div(); + priceTable.getStyle().set("width", "100%"); - // Show only net sum, VAT sums, and total amount without individual services - summaryInfo.add(new HorizontalLayout(new Span(getTranslation("createinvoice.summary.net")), - new Span(netAmount.setScale(2, RoundingMode.HALF_UP) + " €"))); - summaryInfo.add(new HorizontalLayout( - new Span(getTranslation("createinvoice.summary.vat", - vatRate.multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP).toString())), - new Span(vatAmount.setScale(2, RoundingMode.HALF_UP) + " €"))); - summaryInfo.add(new HorizontalLayout(new Span(getTranslation("createinvoice.summary.total")), - new Span(totalAmount.setScale(2, RoundingMode.HALF_UP) + " €"))); + priceTable.add(createPriceRow(getTranslation("createinvoice.summary.net") + ":", + netAmount.setScale(2, RoundingMode.HALF_UP) + " €", false)); + priceTable.add(createPriceRow( + getTranslation("createinvoice.summary.vat", + vatRate.multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP).toString()) + ":", + vatAmount.setScale(2, RoundingMode.HALF_UP) + " €", false)); + priceTable.add(createPriceRow(getTranslation("createinvoice.summary.total") + ":", + totalAmount.setScale(2, RoundingMode.HALF_UP) + " €", true)); - section.add(summaryInfo); + section.add(priceTable); return section; } + private Div createPriceRow(String label, String value, boolean bold) { + Div row = new Div(); + row.getStyle().set("display", "flex").set("justify-content", "space-between").set("padding", "4px 0"); + + Span labelSpan = new Span(label); + labelSpan.getStyle().set("padding-right", "8px"); + + Span valueSpan = new Span(value); + valueSpan.getStyle().set("white-space", "nowrap"); + if (bold) { + labelSpan.getStyle().set("font-weight", "bold"); + valueSpan.getStyle().set("font-weight", "bold"); + } + + row.add(labelSpan, valueSpan); + return row; + } + private BigDecimal calculateServicePrice(Service service) { if (service.getCalculationBasis() == null) { return null; diff --git a/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java b/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java index dd69854..a92bbcc 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java @@ -271,9 +271,15 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has // Preis basierend auf den hinterlegten Leistungen berechnen PriceCalculationResult priceResult = calculatePriceFromServices(job); - infoBox.add(new Span(getTranslation("jobsummary.info.netto") + ": " + formatPrice(priceResult.netAmount()))); - infoBox.add(new Span(getTranslation("jobsummary.info.ust") + ": " + formatPrice(priceResult.vatAmount()))); - infoBox.add(new Span(getTranslation("jobsummary.info.gesamt") + ": " + formatPrice(priceResult.totalAmount()))); + + Div priceTable = new Div(); + priceTable.getStyle().set("width", "100%"); + + priceTable.add(createPriceRow(getTranslation("jobsummary.info.netto") + ":", formatPrice(priceResult.netAmount()), false)); + priceTable.add(createPriceRow(getTranslation("jobsummary.info.ust") + ":", formatPrice(priceResult.vatAmount()), false)); + priceTable.add(createPriceRow(getTranslation("jobsummary.info.gesamt") + ":", formatPrice(priceResult.totalAmount()), true)); + + infoBox.add(priceTable); if (job.getRemark() != null && !job.getRemark().isBlank()) { infoBox.add(new Span(getTranslation("jobsummary.info.bemerkung") + ": " + job.getRemark())); @@ -411,6 +417,24 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has return nf.format(price); } + private Div createPriceRow(String label, String value, boolean bold) { + Div row = new Div(); + row.getStyle().set("display", "flex").set("justify-content", "space-between").set("padding", "4px 0"); + + Span labelSpan = new Span(label); + labelSpan.getStyle().set("padding-right", "8px"); + + Span valueSpan = new Span(value); + valueSpan.getStyle().set("white-space", "nowrap"); + if (bold) { + labelSpan.getStyle().set("font-weight", "bold"); + valueSpan.getStyle().set("font-weight", "bold"); + } + + row.add(labelSpan, valueSpan); + return row; + } + private String resolveAppUserName(String appUserIdString) { try { ObjectId id = new ObjectId(appUserIdString); diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 19b902f..308a840 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -625,6 +625,7 @@ jobs.notification.deleted=Auftrag {0} wurde gelöscht jobs.notification.delete.error=Fehler beim Löschen: {0} # Create Invoice +createinvoice.title=Rechnung erstellen \u2013 Auftrag {0} createinvoice.error.invalidid=Ungültige Job-ID createinvoice.error.notfound=Job nicht gefunden createinvoice.button.create=Rechnung erstellen @@ -641,6 +642,7 @@ createinvoice.route.duration=Fahrtzeit createinvoice.column.service=Leistung createinvoice.column.basis=Berechnungsbasis createinvoice.summary.net=Nettosumme +createinvoice.summary.vat=MwSt. ({0}%) createinvoice.summary.total=Gesamtsumme createinvoice.notification.noservices=Bitte wählen Sie mindestens eine Leistung aus createinvoice.notification.nouser=Benutzer nicht gefunden diff --git a/src/main/resources/messages_en.properties b/src/main/resources/messages_en.properties index 2f7b1b2..830b5f2 100644 --- a/src/main/resources/messages_en.properties +++ b/src/main/resources/messages_en.properties @@ -625,6 +625,7 @@ jobs.notification.deleted=Job {0} deleted jobs.notification.delete.error=Error deleting job: {0} # Create Invoice +createinvoice.title=Create Invoice \u2013 Job {0} createinvoice.error.invalidid=Invalid Job ID createinvoice.error.notfound=Job not found createinvoice.button.create=Create Invoice @@ -641,6 +642,7 @@ createinvoice.route.duration=Duration createinvoice.column.service=Service createinvoice.column.basis=Calculation Basis createinvoice.summary.net=Net Total +createinvoice.summary.vat=VAT ({0}%) createinvoice.summary.total=Total Amount createinvoice.notification.noservices=Please select at least one service createinvoice.notification.nouser=User not found diff --git a/src/main/resources/messages_es.properties b/src/main/resources/messages_es.properties index a33d253..29f71fc 100644 --- a/src/main/resources/messages_es.properties +++ b/src/main/resources/messages_es.properties @@ -625,6 +625,7 @@ jobs.notification.deleted=Trabajo {0} eliminado jobs.notification.delete.error=Error al eliminar trabajo: {0} # Create Invoice +createinvoice.title=Crear Factura \u2013 Trabajo {0} createinvoice.error.invalidid=ID de Trabajo Inválido createinvoice.error.notfound=Trabajo no encontrado createinvoice.button.create=Crear Factura @@ -641,6 +642,7 @@ createinvoice.route.duration=Duración createinvoice.column.service=Servicio createinvoice.column.basis=Base de Cálculo createinvoice.summary.net=Total Neto +createinvoice.summary.vat=IVA ({0}%) createinvoice.summary.total=Total General createinvoice.notification.noservices=Por favor seleccione al menos un servicio createinvoice.notification.nouser=Usuario no encontrado diff --git a/src/main/resources/messages_fr.properties b/src/main/resources/messages_fr.properties index 379a053..2740701 100644 --- a/src/main/resources/messages_fr.properties +++ b/src/main/resources/messages_fr.properties @@ -625,6 +625,7 @@ jobs.notification.deleted=Emploi {0} supprimé jobs.notification.delete.error=Erreur lors de la suppression : {0} # Create Invoice +createinvoice.title=Cr\u00e9er une Facture \u2013 Travail {0} createinvoice.error.invalidid=ID d'Emploi Invalide createinvoice.error.notfound=Emploi non trouvé createinvoice.button.create=Créer une Facture @@ -640,7 +641,8 @@ createinvoice.route.distance=Distance createinvoice.route.duration=Durée createinvoice.column.service=Service createinvoice.column.basis=Base de Calcul -createsummary.net=Total Net +createinvoice.summary.net=Total Net +createinvoice.summary.vat=TVA ({0}%) createinvoice.summary.total=Montant Total createinvoice.notification.noservices=Veuillez sélectionner au moins un service createinvoice.notification.nouser=Utilisateur non trouvé