feat: adapt station pricing and route handling
This commit is contained in:
@@ -148,6 +148,10 @@ public class Job {
|
|||||||
@Field("service_ids")
|
@Field("service_ids")
|
||||||
private List<String> serviceIds;
|
private List<String> serviceIds;
|
||||||
|
|
||||||
|
// Ausgewählte Leistungen inkl. zugeordneter Lieferstation und Berechnungsbasis
|
||||||
|
@Field("selected_services")
|
||||||
|
private List<JobServiceSelection> selectedServices = new ArrayList<>();
|
||||||
|
|
||||||
// Streckeninformation für die Rechnung (in km)
|
// Streckeninformation für die Rechnung (in km)
|
||||||
@Field("route_distance_km")
|
@Field("route_distance_km")
|
||||||
private Double routeDistanceKm;
|
private Double routeDistanceKm;
|
||||||
@@ -227,4 +231,4 @@ public class Job {
|
|||||||
this.deliveryTime = first.getDeliveryTime();
|
this.deliveryTime = first.getDeliveryTime();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package de.assecutor.votianlt.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.data.mongodb.core.mapping.Field;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class JobServiceSelection {
|
||||||
|
|
||||||
|
@Field("service_id")
|
||||||
|
private String serviceId;
|
||||||
|
|
||||||
|
@Field("delivery_station_order")
|
||||||
|
private Integer deliveryStationOrder;
|
||||||
|
|
||||||
|
@Field("route_distance_km")
|
||||||
|
private Double routeDistanceKm;
|
||||||
|
|
||||||
|
@Field("route_duration_seconds")
|
||||||
|
private Integer routeDurationSeconds;
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import java.math.BigDecimal;
|
|||||||
|
|
||||||
@Document(collection = "services")
|
@Document(collection = "services")
|
||||||
public class Service {
|
public class Service {
|
||||||
|
public static final BigDecimal FIXED_VAT_RATE = new BigDecimal("0.19");
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
private String id;
|
private String id;
|
||||||
@@ -16,7 +17,6 @@ public class Service {
|
|||||||
private BigDecimal price; // For FLAT_RATE services
|
private BigDecimal price; // For FLAT_RATE services
|
||||||
private BigDecimal pricePerKilometer; // For DISTANCE services - price per kilometer
|
private BigDecimal pricePerKilometer; // For DISTANCE services - price per kilometer
|
||||||
private BigDecimal pricePer15Minutes; // For TIME services - price per 15 minutes
|
private BigDecimal pricePer15Minutes; // For TIME services - price per 15 minutes
|
||||||
private BigDecimal vatRate;
|
|
||||||
private boolean mandatory;
|
private boolean mandatory;
|
||||||
|
|
||||||
public enum CalculationBasis {
|
public enum CalculationBasis {
|
||||||
@@ -36,7 +36,6 @@ public class Service {
|
|||||||
this.userId = userId;
|
this.userId = userId;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.calculationBasis = calculationBasis;
|
this.calculationBasis = calculationBasis;
|
||||||
this.vatRate = vatRate;
|
|
||||||
this.mandatory = mandatory;
|
this.mandatory = mandatory;
|
||||||
|
|
||||||
// Set the appropriate price field based on calculation basis
|
// Set the appropriate price field based on calculation basis
|
||||||
@@ -86,11 +85,10 @@ public class Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public BigDecimal getVatRate() {
|
public BigDecimal getVatRate() {
|
||||||
return vatRate;
|
return FIXED_VAT_RATE;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setVatRate(BigDecimal vatRate) {
|
public void setVatRate(BigDecimal vatRate) {
|
||||||
this.vatRate = vatRate;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public BigDecimal getPrice() {
|
public BigDecimal getPrice() {
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import de.assecutor.votianlt.pages.service.AddressValidationService;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -199,7 +198,6 @@ public class DeliveryStationDialog extends Dialog {
|
|||||||
private Span tasksTabError;
|
private Span tasksTabError;
|
||||||
|
|
||||||
private final DeliveryStationTile.TranslationHelper translationHelper;
|
private final DeliveryStationTile.TranslationHelper translationHelper;
|
||||||
private final AddressValidationService addressValidationService;
|
|
||||||
private final java.util.Map<String, Customer> companyAddressOptions = new java.util.LinkedHashMap<>();
|
private final java.util.Map<String, Customer> companyAddressOptions = new java.util.LinkedHashMap<>();
|
||||||
|
|
||||||
public DeliveryStationDialog(String dialogTitle, List<Customer> customers,
|
public DeliveryStationDialog(String dialogTitle, List<Customer> customers,
|
||||||
@@ -208,8 +206,6 @@ public class DeliveryStationDialog extends Dialog {
|
|||||||
AddressValidationService addressValidationService) {
|
AddressValidationService addressValidationService) {
|
||||||
|
|
||||||
this.translationHelper = translationHelper;
|
this.translationHelper = translationHelper;
|
||||||
this.addressValidationService = addressValidationService;
|
|
||||||
|
|
||||||
setHeaderTitle(dialogTitle);
|
setHeaderTitle(dialogTitle);
|
||||||
setCloseOnOutsideClick(false);
|
setCloseOnOutsideClick(false);
|
||||||
setWidth("960px");
|
setWidth("960px");
|
||||||
@@ -419,6 +415,7 @@ public class DeliveryStationDialog extends Dialog {
|
|||||||
if (data == null)
|
if (data == null)
|
||||||
return;
|
return;
|
||||||
String companyOption = findCompanyOptionLabel(data);
|
String companyOption = findCompanyOptionLabel(data);
|
||||||
|
boolean customerSelectedFromOptions = companyOption != null;
|
||||||
if (companyOption != null) {
|
if (companyOption != null) {
|
||||||
company.setValue(companyOption);
|
company.setValue(companyOption);
|
||||||
} else if (data.getCompany() != null) {
|
} else if (data.getCompany() != null) {
|
||||||
@@ -442,7 +439,8 @@ public class DeliveryStationDialog extends Dialog {
|
|||||||
zip.setValue(data.getZip());
|
zip.setValue(data.getZip());
|
||||||
if (data.getCity() != null)
|
if (data.getCity() != null)
|
||||||
city.setValue(data.getCity());
|
city.setValue(data.getCity());
|
||||||
saveAddress.setValue(data.isSaveAddress());
|
saveAddress.setValue(customerSelectedFromOptions ? false : data.isSaveAddress());
|
||||||
|
updateSaveAddressState(customerSelectedFromOptions);
|
||||||
|
|
||||||
// Load tasks into dialog state
|
// Load tasks into dialog state
|
||||||
if (data.getTasks() != null && !data.getTasks().isEmpty()) {
|
if (data.getTasks() != null && !data.getTasks().isEmpty()) {
|
||||||
@@ -562,6 +560,7 @@ public class DeliveryStationDialog extends Dialog {
|
|||||||
|
|
||||||
companyField.addValueChangeListener(event -> {
|
companyField.addValueChangeListener(event -> {
|
||||||
Customer customer = companyAddressOptions.get(event.getValue());
|
Customer customer = companyAddressOptions.get(event.getValue());
|
||||||
|
updateSaveAddressState(customer != null);
|
||||||
if (customer == null) {
|
if (customer == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -589,7 +588,20 @@ public class DeliveryStationDialog extends Dialog {
|
|||||||
city.setValue(customer.getCity());
|
city.setValue(customer.getCity());
|
||||||
});
|
});
|
||||||
|
|
||||||
companyField.addCustomValueSetListener(event -> companyField.setValue(event.getDetail()));
|
companyField.addCustomValueSetListener(event -> {
|
||||||
|
companyField.setValue(event.getDetail());
|
||||||
|
updateSaveAddressState(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSaveAddressState(boolean customerSelectedFromOptions) {
|
||||||
|
if (customerSelectedFromOptions) {
|
||||||
|
saveAddress.setValue(false);
|
||||||
|
saveAddress.setEnabled(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveAddress.setEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildCompanyAddressLabel(Customer customer) {
|
private String buildCompanyAddressLabel(Customer customer) {
|
||||||
|
|||||||
@@ -250,14 +250,10 @@ public class PickupStationDialog extends Dialog {
|
|||||||
private Span cargoTabError;
|
private Span cargoTabError;
|
||||||
|
|
||||||
private final DeliveryStationTile.TranslationHelper translationHelper;
|
private final DeliveryStationTile.TranslationHelper translationHelper;
|
||||||
private final AddressValidationService addressValidationService;
|
|
||||||
|
|
||||||
public PickupStationDialog(String dialogTitle, List<Customer> customers,
|
public PickupStationDialog(String dialogTitle, List<Customer> customers,
|
||||||
DeliveryStationTile.TranslationHelper translationHelper, SaveListener saveListener,
|
DeliveryStationTile.TranslationHelper translationHelper, SaveListener saveListener,
|
||||||
List<AppUser> availableAppUsers, AddressValidationService addressValidationService) {
|
List<AppUser> availableAppUsers, AddressValidationService addressValidationService) {
|
||||||
|
|
||||||
this.addressValidationService = addressValidationService;
|
|
||||||
|
|
||||||
this.translationHelper = translationHelper;
|
this.translationHelper = translationHelper;
|
||||||
|
|
||||||
setHeaderTitle(dialogTitle);
|
setHeaderTitle(dialogTitle);
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import com.vaadin.flow.theme.lumo.LumoUtility;
|
|||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import de.assecutor.votianlt.model.Job;
|
import de.assecutor.votianlt.model.Job;
|
||||||
|
import de.assecutor.votianlt.model.JobServiceSelection;
|
||||||
import de.assecutor.votianlt.model.task.BarcodeTask;
|
import de.assecutor.votianlt.model.task.BarcodeTask;
|
||||||
import de.assecutor.votianlt.model.task.BaseTask;
|
import de.assecutor.votianlt.model.task.BaseTask;
|
||||||
import de.assecutor.votianlt.model.task.CommentTask;
|
import de.assecutor.votianlt.model.task.CommentTask;
|
||||||
@@ -81,6 +82,32 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
private static final DateTimeFormatter PICKUP_PREVIEW_DATE_FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy");
|
private static final DateTimeFormatter PICKUP_PREVIEW_DATE_FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy");
|
||||||
private static final DateTimeFormatter PICKUP_PREVIEW_TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm");
|
private static final DateTimeFormatter PICKUP_PREVIEW_TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm");
|
||||||
|
|
||||||
|
private static final class SelectedServiceEntry {
|
||||||
|
private final Service service;
|
||||||
|
private Integer deliveryStationOrder;
|
||||||
|
|
||||||
|
private SelectedServiceEntry(Service service, Integer deliveryStationOrder) {
|
||||||
|
this.service = service;
|
||||||
|
this.deliveryStationOrder = deliveryStationOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Service getService() {
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getDeliveryStationOrder() {
|
||||||
|
return deliveryStationOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDeliveryStationOrder(Integer deliveryStationOrder) {
|
||||||
|
this.deliveryStationOrder = deliveryStationOrder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record RouteCalculationBundle(RouteCalculationResult totalRoute,
|
||||||
|
Map<Integer, RouteCalculationResult> deliveryRoutes) {
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getPageTitle() {
|
public String getPageTitle() {
|
||||||
return getTranslation("page.title.job.create");
|
return getTranslation("page.title.job.create");
|
||||||
@@ -115,8 +142,12 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
private final List<StationTile> deliveryStationTilesList = new ArrayList<>();
|
private final List<StationTile> deliveryStationTilesList = new ArrayList<>();
|
||||||
private final List<DeliveryStation> deliveryStationsState = new ArrayList<>();
|
private final List<DeliveryStation> deliveryStationsState = new ArrayList<>();
|
||||||
private final List<Boolean> deliveryStationsSaveAddress = new ArrayList<>();
|
private final List<Boolean> deliveryStationsSaveAddress = new ArrayList<>();
|
||||||
|
private final List<Div> deliveryStationSlotList = new ArrayList<>();
|
||||||
|
private final List<Span> deliveryStationDistanceChips = new ArrayList<>();
|
||||||
private Div stationsGridContainer;
|
private Div stationsGridContainer;
|
||||||
private Div addStationButton;
|
private Div addStationButton;
|
||||||
|
private Div addStationButtonSlot;
|
||||||
|
private Div pickupStationSlot;
|
||||||
private StationTile pickupTile;
|
private StationTile pickupTile;
|
||||||
private static final int MAX_DELIVERY_STATIONS = 7;
|
private static final int MAX_DELIVERY_STATIONS = 7;
|
||||||
|
|
||||||
@@ -125,10 +156,9 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
private ComboBox<AppUser> appUser;
|
private ComboBox<AppUser> appUser;
|
||||||
|
|
||||||
// Services for the job
|
// Services for the job
|
||||||
private Grid<Service> servicesGrid;
|
private Grid<SelectedServiceEntry> servicesGrid;
|
||||||
private final List<Service> selectedServices = new ArrayList<>();
|
private final List<SelectedServiceEntry> selectedServices = new ArrayList<>();
|
||||||
private Span netTotalLabel;
|
private Span netTotalLabel;
|
||||||
private Span vatTotalLabel;
|
|
||||||
private Span grossTotalLabel;
|
private Span grossTotalLabel;
|
||||||
|
|
||||||
// Route distance display
|
// Route distance display
|
||||||
@@ -173,6 +203,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
private RouteCalculationResult routeCalculationResult;
|
private RouteCalculationResult routeCalculationResult;
|
||||||
private boolean pickupAddressValidatedByGoogle;
|
private boolean pickupAddressValidatedByGoogle;
|
||||||
private final List<Boolean> deliveryStationsValidatedByGoogle = new ArrayList<>();
|
private final List<Boolean> deliveryStationsValidatedByGoogle = new ArrayList<>();
|
||||||
|
private final Map<Integer, RouteCalculationResult> pickupToDeliveryRouteResults = new HashMap<>();
|
||||||
|
|
||||||
public AddJobView(AddJobService addJobService, AddCustomerService addCustomerService,
|
public AddJobView(AddJobService addJobService, AddCustomerService addCustomerService,
|
||||||
CustomerService customerService, AppUserService appUserService, TaskTemplateService taskTemplateService,
|
CustomerService customerService, AppUserService appUserService, TaskTemplateService taskTemplateService,
|
||||||
@@ -342,10 +373,12 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
// Pickup tile (always present)
|
// Pickup tile (always present)
|
||||||
pickupTile = new StationTile(StationTile.StationType.PICKUP, 0, getTranslation("addjob.section.pickup"), false);
|
pickupTile = new StationTile(StationTile.StationType.PICKUP, 0, getTranslation("addjob.section.pickup"), false);
|
||||||
pickupTile.setClickListener(tile -> openPickupDialog());
|
pickupTile.setClickListener(tile -> openPickupDialog());
|
||||||
stationsGridContainer.add(pickupTile);
|
pickupStationSlot = createStationSlot(pickupTile, null);
|
||||||
|
stationsGridContainer.add(pickupStationSlot);
|
||||||
|
|
||||||
// "+" add station button tile (must be created before addDeliveryStationTile)
|
// "+" add station button tile (must be created before addDeliveryStationTile)
|
||||||
addStationButton = createAddStationButton();
|
addStationButton = createAddStationButton();
|
||||||
|
addStationButtonSlot = createStationSlot(addStationButton, null);
|
||||||
|
|
||||||
// Add first delivery station tile (this will also add the "+" button)
|
// Add first delivery station tile (this will also add the "+" button)
|
||||||
addDeliveryStationTile();
|
addDeliveryStationTile();
|
||||||
@@ -454,6 +487,12 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
manualDurationInput.setMin(0);
|
manualDurationInput.setMin(0);
|
||||||
manualDurationInput.setStep(1);
|
manualDurationInput.setStep(1);
|
||||||
manualDurationInput.setClearButtonVisible(true);
|
manualDurationInput.setClearButtonVisible(true);
|
||||||
|
manualDurationInput.addValueChangeListener(e -> {
|
||||||
|
updatePriceSummary();
|
||||||
|
if (servicesGrid != null) {
|
||||||
|
servicesGrid.getDataProvider().refreshAll();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
manualInputRow.add(manualDistanceInput, manualDurationInput);
|
manualInputRow.add(manualDistanceInput, manualDurationInput);
|
||||||
|
|
||||||
@@ -476,8 +515,12 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
servicesGrid.setHeight("250px");
|
servicesGrid.setHeight("250px");
|
||||||
servicesGrid.setItems(selectedServices);
|
servicesGrid.setItems(selectedServices);
|
||||||
|
|
||||||
servicesGrid.addColumn(Service::getName).setHeader(getTranslation("common.service")).setSortable(true);
|
servicesGrid.addColumn(entry -> entry.getService().getName()).setHeader(getTranslation("common.service"))
|
||||||
servicesGrid.addColumn(service -> {
|
.setSortable(true);
|
||||||
|
servicesGrid.addColumn(this::formatSelectedServiceStationLabel)
|
||||||
|
.setHeader(getTranslation("addjob.services.deliverystation")).setSortable(false);
|
||||||
|
servicesGrid.addColumn(entry -> {
|
||||||
|
Service service = entry.getService();
|
||||||
if (service.getCalculationBasis() != null) {
|
if (service.getCalculationBasis() != null) {
|
||||||
return switch (service.getCalculationBasis()) {
|
return switch (service.getCalculationBasis()) {
|
||||||
case DISTANCE -> getTranslation("addjob.services.basis.distance");
|
case DISTANCE -> getTranslation("addjob.services.basis.distance");
|
||||||
@@ -487,29 +530,28 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}).setHeader(getTranslation("addjob.services.calculation")).setSortable(true);
|
}).setHeader(getTranslation("addjob.services.calculation")).setSortable(true);
|
||||||
servicesGrid.addColumn(service -> {
|
servicesGrid.addColumn(entry -> {
|
||||||
// Get route distance for distance-based calculations (berechnet oder manuell)
|
Service service = entry.getService();
|
||||||
Double routeDistance = getEffectiveRouteDistance();
|
Double routeDistance = getEffectiveRouteDistance(entry);
|
||||||
BigDecimal price = calculateServicePrice(service, routeDistance);
|
Integer durationSeconds = getEffectiveRouteDuration(entry);
|
||||||
|
BigDecimal price = calculateServicePrice(service, routeDistance, durationSeconds);
|
||||||
if (price.compareTo(BigDecimal.ZERO) > 0) {
|
if (price.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
return price.setScale(2, RoundingMode.HALF_UP) + " €";
|
return price.setScale(2, RoundingMode.HALF_UP) + " €";
|
||||||
}
|
}
|
||||||
// Show price info if no route calculated yet
|
|
||||||
if (service.getCalculationBasis() == Service.CalculationBasis.DISTANCE && routeDistance == null) {
|
if (service.getCalculationBasis() == Service.CalculationBasis.DISTANCE && routeDistance == null) {
|
||||||
return service.getPricePerKilometer().setScale(2, RoundingMode.HALF_UP) + " €/km ("
|
return service.getPricePerKilometer().setScale(2, RoundingMode.HALF_UP) + " €/km ("
|
||||||
+ getTranslation("addjob.services.route.missing") + ")";
|
+ getTranslation("addjob.services.route.missing") + ")";
|
||||||
}
|
}
|
||||||
|
if (service.getCalculationBasis() == Service.CalculationBasis.TIME && durationSeconds == null) {
|
||||||
|
return service.getPricePer15Minutes().setScale(2, RoundingMode.HALF_UP) + " €/15 Min. ("
|
||||||
|
+ getTranslation("addjob.services.route.missing") + ")";
|
||||||
|
}
|
||||||
return service.getEffectivePrice() != null
|
return service.getEffectivePrice() != null
|
||||||
? service.getEffectivePrice().setScale(2, RoundingMode.HALF_UP) + " €"
|
? service.getEffectivePrice().setScale(2, RoundingMode.HALF_UP) + " €"
|
||||||
: "";
|
: "";
|
||||||
}).setHeader(getTranslation("common.price")).setSortable(false);
|
}).setHeader(getTranslation("common.price")).setSortable(false);
|
||||||
servicesGrid.addColumn(service -> {
|
servicesGrid.addComponentColumn(entry -> {
|
||||||
if (service.getVatRate() != null) {
|
Service service = entry.getService();
|
||||||
return service.getVatRate().multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP) + " %";
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}).setHeader(getTranslation("addjob.services.vat")).setSortable(true);
|
|
||||||
servicesGrid.addComponentColumn(service -> {
|
|
||||||
// Verbindliche Leistungen können nicht gelöscht werden
|
// Verbindliche Leistungen können nicht gelöscht werden
|
||||||
if (service.isMandatory()) {
|
if (service.isMandatory()) {
|
||||||
return new Span(""); // Leeres Element statt Löschen-Button
|
return new Span(""); // Leeres Element statt Löschen-Button
|
||||||
@@ -518,7 +560,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
removeButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY,
|
removeButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY,
|
||||||
ButtonVariant.LUMO_SMALL);
|
ButtonVariant.LUMO_SMALL);
|
||||||
removeButton.addClickListener(e -> {
|
removeButton.addClickListener(e -> {
|
||||||
selectedServices.remove(service);
|
selectedServices.remove(entry);
|
||||||
servicesGrid.getDataProvider().refreshAll();
|
servicesGrid.getDataProvider().refreshAll();
|
||||||
updatePriceSummary();
|
updatePriceSummary();
|
||||||
triggerValidation();
|
triggerValidation();
|
||||||
@@ -561,16 +603,6 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
netRow.add(netLabelSpan, netTotalLabel);
|
netRow.add(netLabelSpan, netTotalLabel);
|
||||||
priceTable.add(netRow);
|
priceTable.add(netRow);
|
||||||
|
|
||||||
// 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").set("white-space", "nowrap");
|
|
||||||
vatRow.add(vatLabelSpan, vatTotalLabel);
|
|
||||||
priceTable.add(vatRow);
|
|
||||||
|
|
||||||
// Gross total row
|
// Gross total row
|
||||||
Div grossRow = new Div();
|
Div grossRow = new Div();
|
||||||
grossRow.getStyle().set("display", "flex").set("justify-content", "space-between").set("padding", "4px 0");
|
grossRow.getStyle().set("display", "flex").set("justify-content", "space-between").set("padding", "4px 0");
|
||||||
@@ -630,6 +662,41 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
return button;
|
return button;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Div createStationSlot(Component content, Span distanceChip) {
|
||||||
|
Div slot = new Div();
|
||||||
|
slot.getStyle().set("display", "flex");
|
||||||
|
slot.getStyle().set("flex-direction", "column");
|
||||||
|
slot.getStyle().set("align-items", "center");
|
||||||
|
slot.getStyle().set("gap", "var(--lumo-space-s)");
|
||||||
|
slot.setWidthFull();
|
||||||
|
|
||||||
|
content.getElement().getStyle().set("width", "100%");
|
||||||
|
slot.add(content);
|
||||||
|
|
||||||
|
if (distanceChip != null) {
|
||||||
|
slot.add(distanceChip);
|
||||||
|
}
|
||||||
|
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Span createDeliveryDistanceChip() {
|
||||||
|
Span chip = new Span();
|
||||||
|
chip.getStyle().set("display", "inline-flex");
|
||||||
|
chip.getStyle().set("align-items", "center");
|
||||||
|
chip.getStyle().set("justify-content", "center");
|
||||||
|
chip.getStyle().set("padding", "0.35rem 0.75rem");
|
||||||
|
chip.getStyle().set("border-radius", "999px");
|
||||||
|
chip.getStyle().set("background-color", "var(--lumo-primary-color-10pct)");
|
||||||
|
chip.getStyle().set("border", "1px solid var(--lumo-primary-color-30pct)");
|
||||||
|
chip.getStyle().set("color", "var(--lumo-primary-text-color)");
|
||||||
|
chip.getStyle().set("font-size", "var(--lumo-font-size-s)");
|
||||||
|
chip.getStyle().set("font-weight", "600");
|
||||||
|
chip.getStyle().set("text-align", "center");
|
||||||
|
chip.setVisible(false);
|
||||||
|
return chip;
|
||||||
|
}
|
||||||
|
|
||||||
private void addDeliveryStationTile() {
|
private void addDeliveryStationTile() {
|
||||||
int stationNumber = deliveryStationTilesList.size() + 1;
|
int stationNumber = deliveryStationTilesList.size() + 1;
|
||||||
boolean removable = deliveryStationTilesList.size() > 0; // First station is not removable
|
boolean removable = deliveryStationTilesList.size() > 0; // First station is not removable
|
||||||
@@ -646,15 +713,20 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
tile.setClickListener(t -> openDeliveryDialog(t, stationIndex));
|
tile.setClickListener(t -> openDeliveryDialog(t, stationIndex));
|
||||||
tile.setDeleteListener(this::removeDeliveryStationTile);
|
tile.setDeleteListener(this::removeDeliveryStationTile);
|
||||||
|
|
||||||
|
Span distanceChip = createDeliveryDistanceChip();
|
||||||
|
Div stationSlot = createStationSlot(tile, distanceChip);
|
||||||
|
|
||||||
deliveryStationTilesList.add(tile);
|
deliveryStationTilesList.add(tile);
|
||||||
|
deliveryStationSlotList.add(stationSlot);
|
||||||
|
deliveryStationDistanceChips.add(distanceChip);
|
||||||
|
|
||||||
// Rebuild grid: remove plus button, add tile, re-add plus button
|
// Rebuild grid: remove plus button, add tile, re-add plus button
|
||||||
stationsGridContainer.remove(addStationButton);
|
stationsGridContainer.remove(addStationButtonSlot);
|
||||||
stationsGridContainer.add(tile);
|
stationsGridContainer.add(stationSlot);
|
||||||
|
|
||||||
// Hide "+" button if max reached
|
// Hide "+" button if max reached
|
||||||
if (deliveryStationTilesList.size() < MAX_DELIVERY_STATIONS) {
|
if (deliveryStationTilesList.size() < MAX_DELIVERY_STATIONS) {
|
||||||
stationsGridContainer.add(addStationButton);
|
stationsGridContainer.add(addStationButtonSlot);
|
||||||
}
|
}
|
||||||
|
|
||||||
resetRouteInformation();
|
resetRouteInformation();
|
||||||
@@ -683,6 +755,9 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
deliveryStationsSaveAddress.remove(removeIdx);
|
deliveryStationsSaveAddress.remove(removeIdx);
|
||||||
deliveryStationsValidatedByGoogle.remove(removeIdx);
|
deliveryStationsValidatedByGoogle.remove(removeIdx);
|
||||||
deliveryStationTasksState.remove(removeIdx);
|
deliveryStationTasksState.remove(removeIdx);
|
||||||
|
Div removedSlot = deliveryStationSlotList.remove(removeIdx);
|
||||||
|
deliveryStationDistanceChips.remove(removeIdx);
|
||||||
|
pickupToDeliveryRouteResults.remove(removeIdx);
|
||||||
// Re-index tasks state for remaining stations
|
// Re-index tasks state for remaining stations
|
||||||
Map<Integer, List<BaseTask>> reindexed = new HashMap<>();
|
Map<Integer, List<BaseTask>> reindexed = new HashMap<>();
|
||||||
for (Map.Entry<Integer, List<BaseTask>> entry : deliveryStationTasksState.entrySet()) {
|
for (Map.Entry<Integer, List<BaseTask>> entry : deliveryStationTasksState.entrySet()) {
|
||||||
@@ -692,7 +767,28 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
}
|
}
|
||||||
deliveryStationTasksState.clear();
|
deliveryStationTasksState.clear();
|
||||||
deliveryStationTasksState.putAll(reindexed);
|
deliveryStationTasksState.putAll(reindexed);
|
||||||
stationsGridContainer.remove(tile);
|
|
||||||
|
Map<Integer, RouteCalculationResult> reindexedRoutes = new HashMap<>();
|
||||||
|
for (Map.Entry<Integer, RouteCalculationResult> entry : pickupToDeliveryRouteResults.entrySet()) {
|
||||||
|
int oldIdx = entry.getKey();
|
||||||
|
int newIdx = oldIdx > removeIdx ? oldIdx - 1 : oldIdx;
|
||||||
|
reindexedRoutes.put(newIdx, entry.getValue());
|
||||||
|
}
|
||||||
|
pickupToDeliveryRouteResults.clear();
|
||||||
|
pickupToDeliveryRouteResults.putAll(reindexedRoutes);
|
||||||
|
|
||||||
|
for (SelectedServiceEntry selectedService : selectedServices) {
|
||||||
|
Integer stationOrder = selectedService.getDeliveryStationOrder();
|
||||||
|
if (stationOrder == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (stationOrder == removeIdx) {
|
||||||
|
selectedService.setDeliveryStationOrder(deliveryStationsState.isEmpty() ? null : 0);
|
||||||
|
} else if (stationOrder > removeIdx) {
|
||||||
|
selectedService.setDeliveryStationOrder(stationOrder - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stationsGridContainer.remove(removedSlot);
|
||||||
|
|
||||||
// Renumber remaining tiles and update click listeners
|
// Renumber remaining tiles and update click listeners
|
||||||
for (int i = 0; i < deliveryStationTilesList.size(); i++) {
|
for (int i = 0; i < deliveryStationTilesList.size(); i++) {
|
||||||
@@ -710,12 +806,16 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ensure "+" button is visible if under max
|
// Ensure "+" button is visible if under max
|
||||||
if (deliveryStationTilesList.size() < MAX_DELIVERY_STATIONS && addStationButton.getParent().isEmpty()) {
|
if (deliveryStationTilesList.size() < MAX_DELIVERY_STATIONS && addStationButtonSlot.getParent().isEmpty()) {
|
||||||
stationsGridContainer.add(addStationButton);
|
stationsGridContainer.add(addStationButtonSlot);
|
||||||
}
|
}
|
||||||
|
|
||||||
resetRouteInformation();
|
resetRouteInformation();
|
||||||
resetStationsAppliedState();
|
resetStationsAppliedState();
|
||||||
|
if (servicesGrid != null) {
|
||||||
|
servicesGrid.getDataProvider().refreshAll();
|
||||||
|
}
|
||||||
|
updatePriceSummary();
|
||||||
triggerValidation();
|
triggerValidation();
|
||||||
updateTabLabels();
|
updateTabLabels();
|
||||||
});
|
});
|
||||||
@@ -1075,7 +1175,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
private void openAddServiceDialog() {
|
private void openAddServiceDialog() {
|
||||||
Dialog dialog = new Dialog();
|
Dialog dialog = new Dialog();
|
||||||
dialog.setHeaderTitle(getTranslation("addjob.services.dialog.title"));
|
dialog.setHeaderTitle(getTranslation("addjob.services.dialog.title"));
|
||||||
dialog.setWidth("500px");
|
dialog.setWidth("560px");
|
||||||
|
|
||||||
VerticalLayout dialogContent = new VerticalLayout();
|
VerticalLayout dialogContent = new VerticalLayout();
|
||||||
dialogContent.setPadding(true);
|
dialogContent.setPadding(true);
|
||||||
@@ -1099,7 +1199,18 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
serviceCombo.setPlaceholder(getTranslation("addjob.services.dialog.placeholder"));
|
serviceCombo.setPlaceholder(getTranslation("addjob.services.dialog.placeholder"));
|
||||||
serviceCombo.setRequired(true);
|
serviceCombo.setRequired(true);
|
||||||
|
|
||||||
dialogContent.add(serviceCombo);
|
ComboBox<Integer> deliveryStationCombo = new ComboBox<>(getTranslation("addjob.services.deliverystation"));
|
||||||
|
deliveryStationCombo.setWidthFull();
|
||||||
|
deliveryStationCombo.setRequired(true);
|
||||||
|
deliveryStationCombo.setRequiredIndicatorVisible(true);
|
||||||
|
deliveryStationCombo.setItems(getAvailableDeliveryStationOrders());
|
||||||
|
deliveryStationCombo.setItemLabelGenerator(this::buildDeliveryStationSelectionLabel);
|
||||||
|
deliveryStationCombo.setPlaceholder(getTranslation("addjob.services.dialog.station.placeholder"));
|
||||||
|
if (!deliveryStationsState.isEmpty()) {
|
||||||
|
deliveryStationCombo.setValue(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogContent.add(serviceCombo, deliveryStationCombo);
|
||||||
|
|
||||||
HorizontalLayout buttonLayout = new HorizontalLayout();
|
HorizontalLayout buttonLayout = new HorizontalLayout();
|
||||||
buttonLayout.setWidthFull();
|
buttonLayout.setWidthFull();
|
||||||
@@ -1110,8 +1221,8 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
cancelButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
cancelButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||||
|
|
||||||
Button addButton = new Button(getTranslation("addjob.services.dialog.add"), e -> {
|
Button addButton = new Button(getTranslation("addjob.services.dialog.add"), e -> {
|
||||||
if (serviceCombo.getValue() != null) {
|
if (serviceCombo.getValue() != null && deliveryStationCombo.getValue() != null) {
|
||||||
selectedServices.add(serviceCombo.getValue());
|
selectedServices.add(new SelectedServiceEntry(serviceCombo.getValue(), deliveryStationCombo.getValue()));
|
||||||
servicesGrid.getDataProvider().refreshAll();
|
servicesGrid.getDataProvider().refreshAll();
|
||||||
updatePriceSummary();
|
updatePriceSummary();
|
||||||
triggerValidation();
|
triggerValidation();
|
||||||
@@ -1130,33 +1241,61 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
|
|
||||||
private void updatePriceSummary() {
|
private void updatePriceSummary() {
|
||||||
BigDecimal netTotal = BigDecimal.ZERO;
|
BigDecimal netTotal = BigDecimal.ZERO;
|
||||||
BigDecimal vatTotal = BigDecimal.ZERO;
|
|
||||||
BigDecimal grossTotal = BigDecimal.ZERO;
|
BigDecimal grossTotal = BigDecimal.ZERO;
|
||||||
|
|
||||||
// Get route distance for distance-based calculations (berechnet oder manuell)
|
for (SelectedServiceEntry entry : selectedServices) {
|
||||||
Double routeDistance = getEffectiveRouteDistance();
|
Service service = entry.getService();
|
||||||
|
BigDecimal price = calculateServicePrice(service, getEffectiveRouteDistance(entry),
|
||||||
for (Service service : selectedServices) {
|
getEffectiveRouteDuration(entry));
|
||||||
BigDecimal price = calculateServicePrice(service, routeDistance);
|
BigDecimal vatRate = Service.FIXED_VAT_RATE;
|
||||||
BigDecimal vatRate = service.getVatRate() != null ? service.getVatRate() : BigDecimal.ZERO;
|
|
||||||
|
|
||||||
netTotal = netTotal.add(price);
|
netTotal = netTotal.add(price);
|
||||||
BigDecimal vatAmount = price.multiply(vatRate);
|
BigDecimal vatAmount = price.multiply(vatRate);
|
||||||
vatTotal = vatTotal.add(vatAmount);
|
|
||||||
grossTotal = grossTotal.add(price.add(vatAmount));
|
grossTotal = grossTotal.add(price.add(vatAmount));
|
||||||
}
|
}
|
||||||
|
|
||||||
netTotalLabel.setText(netTotal.setScale(2, RoundingMode.HALF_UP).toString().replace(".", ",") + " €");
|
netTotalLabel.setText(netTotal.setScale(2, RoundingMode.HALF_UP).toString().replace(".", ",") + " €");
|
||||||
vatTotalLabel.setText(vatTotal.setScale(2, RoundingMode.HALF_UP).toString().replace(".", ",") + " €");
|
|
||||||
grossTotalLabel.setText(grossTotal.setScale(2, RoundingMode.HALF_UP).toString().replace(".", ",") + " €");
|
grossTotalLabel.setText(grossTotal.setScale(2, RoundingMode.HALF_UP).toString().replace(".", ",") + " €");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private List<Integer> getAvailableDeliveryStationOrders() {
|
||||||
* Calculates the actual price for a service based on its calculation basis and
|
List<Integer> stationOrders = new ArrayList<>();
|
||||||
* route distance (for distance-based services).
|
for (int i = 0; i < deliveryStationsState.size(); i++) {
|
||||||
*/
|
stationOrders.add(i);
|
||||||
private BigDecimal calculateServicePrice(Service service, Double routeDistance) {
|
}
|
||||||
return calculateServicePrice(service, routeDistance, null);
|
return stationOrders;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatSelectedServiceStationLabel(SelectedServiceEntry entry) {
|
||||||
|
return formatDeliveryStationLabel(entry.getDeliveryStationOrder());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildDeliveryStationSelectionLabel(Integer stationOrder) {
|
||||||
|
if (stationOrder == null || stationOrder < 0 || stationOrder >= deliveryStationsState.size()) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
DeliveryStation station = deliveryStationsState.get(stationOrder);
|
||||||
|
StringBuilder label = new StringBuilder(getTranslation("addjob.station.delivery", stationOrder + 1));
|
||||||
|
|
||||||
|
String city = trimToNull(station.getCity());
|
||||||
|
if (city != null) {
|
||||||
|
label.append(" - ").append(city);
|
||||||
|
} else {
|
||||||
|
String company = trimToNull(station.getCompany());
|
||||||
|
if (company != null) {
|
||||||
|
label.append(" - ").append(company);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return label.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatDeliveryStationLabel(Integer stationOrder) {
|
||||||
|
if (stationOrder == null || stationOrder < 0) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
return "Lieferstation " + (stationOrder + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private BigDecimal calculateServicePrice(Service service, Double routeDistance, Integer durationSeconds) {
|
private BigDecimal calculateServicePrice(Service service, Double routeDistance, Integer durationSeconds) {
|
||||||
@@ -1189,6 +1328,44 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Double getEffectiveRouteDistance(SelectedServiceEntry entry) {
|
||||||
|
RouteCalculationResult stationRoute = getSelectedServiceRoute(entry);
|
||||||
|
if (stationRoute != null && stationRoute.isValid()) {
|
||||||
|
return stationRoute.getDistanceKm();
|
||||||
|
}
|
||||||
|
if (routeCalculationResult == null || !routeCalculationResult.isValid()) {
|
||||||
|
return getEffectiveRouteDistance();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer getEffectiveRouteDuration(SelectedServiceEntry entry) {
|
||||||
|
RouteCalculationResult stationRoute = getSelectedServiceRoute(entry);
|
||||||
|
if (stationRoute != null && stationRoute.isValid()) {
|
||||||
|
return stationRoute.getDurationSeconds();
|
||||||
|
}
|
||||||
|
if (routeCalculationResult == null || !routeCalculationResult.isValid()) {
|
||||||
|
return getEffectiveRouteDuration();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RouteCalculationResult getSelectedServiceRoute(SelectedServiceEntry entry) {
|
||||||
|
if (entry == null || entry.getDeliveryStationOrder() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return pickupToDeliveryRouteResults.get(entry.getDeliveryStationOrder());
|
||||||
|
}
|
||||||
|
|
||||||
|
private JobServiceSelection toJobServiceSelection(SelectedServiceEntry entry) {
|
||||||
|
JobServiceSelection selection = new JobServiceSelection();
|
||||||
|
selection.setServiceId(entry.getService().getId());
|
||||||
|
selection.setDeliveryStationOrder(entry.getDeliveryStationOrder());
|
||||||
|
selection.setRouteDistanceKm(getEffectiveRouteDistance(entry));
|
||||||
|
selection.setRouteDurationSeconds(getEffectiveRouteDuration(entry));
|
||||||
|
return selection;
|
||||||
|
}
|
||||||
|
|
||||||
// createPickupSection(), togglePickupCollapse(), updateAddStationButtonSize()
|
// createPickupSection(), togglePickupCollapse(), updateAddStationButtonSize()
|
||||||
// removed - replaced by StationTile + StationDialog
|
// removed - replaced by StationTile + StationDialog
|
||||||
|
|
||||||
@@ -1577,15 +1754,15 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
job.syncFlatDeliveryFieldsFromStations();
|
job.syncFlatDeliveryFieldsFromStations();
|
||||||
|
|
||||||
// Store selected service IDs in job for invoice creation
|
// Store selected service IDs in job for invoice creation
|
||||||
job.setServiceIds(selectedServices.stream().map(Service::getId).toList());
|
job.setServiceIds(selectedServices.stream().map(entry -> entry.getService().getId()).toList());
|
||||||
|
job.setSelectedServices(selectedServices.stream().map(this::toJobServiceSelection).toList());
|
||||||
|
|
||||||
// Validate all required fields using the binder
|
// Validate all required fields using the binder
|
||||||
if (binder.writeBeanIfValid(job)) {
|
if (binder.writeBeanIfValid(job)) {
|
||||||
// Preis nach dem Binder-Call berechnen (damit er nicht überschrieben wird)
|
// Preis nach dem Binder-Call berechnen (damit er nicht überschrieben wird)
|
||||||
Double routeDistance = getEffectiveRouteDistance();
|
|
||||||
Integer durationSeconds = getEffectiveRouteDuration();
|
|
||||||
BigDecimal netTotal = selectedServices.stream()
|
BigDecimal netTotal = selectedServices.stream()
|
||||||
.map(s -> calculateServicePrice(s, routeDistance, durationSeconds))
|
.map(entry -> calculateServicePrice(entry.getService(), getEffectiveRouteDistance(entry),
|
||||||
|
getEffectiveRouteDuration(entry)))
|
||||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
job.setPrice(netTotal);
|
job.setPrice(netTotal);
|
||||||
|
|
||||||
@@ -1706,7 +1883,8 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
List<Service> mandatoryServices = userServices.stream().filter(Service::isMandatory).toList();
|
List<Service> mandatoryServices = userServices.stream().filter(Service::isMandatory).toList();
|
||||||
|
|
||||||
if (!mandatoryServices.isEmpty()) {
|
if (!mandatoryServices.isEmpty()) {
|
||||||
selectedServices.addAll(mandatoryServices);
|
mandatoryServices.stream().map(service -> new SelectedServiceEntry(service, 0))
|
||||||
|
.forEach(selectedServices::add);
|
||||||
if (servicesGrid != null) {
|
if (servicesGrid != null) {
|
||||||
servicesGrid.getDataProvider().refreshAll();
|
servicesGrid.getDataProvider().refreshAll();
|
||||||
}
|
}
|
||||||
@@ -1908,7 +2086,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
UI ui = UI.getCurrent();
|
UI ui = UI.getCurrent();
|
||||||
loadingDialog.open();
|
loadingDialog.open();
|
||||||
|
|
||||||
CompletableFuture.supplyAsync(this::calculateRouteAcrossAllStations).whenComplete((routeResult, throwable) -> {
|
CompletableFuture.supplyAsync(this::calculateRouteBundle).whenComplete((routeBundle, throwable) -> {
|
||||||
if (ui == null) {
|
if (ui == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1921,8 +2099,9 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RouteCalculationResult routeResult = routeBundle != null ? routeBundle.totalRoute() : null;
|
||||||
if (routeResult != null && routeResult.isValid()) {
|
if (routeResult != null && routeResult.isValid()) {
|
||||||
applyCalculatedRoute(routeResult);
|
applyCalculatedRoutes(routeBundle);
|
||||||
showRouteSummaryDialog(routeResult);
|
showRouteSummaryDialog(routeResult);
|
||||||
} else {
|
} else {
|
||||||
String message = routeResult != null && routeResult.getRouteMessage() != null
|
String message = routeResult != null && routeResult.getRouteMessage() != null
|
||||||
@@ -1950,36 +2129,86 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
return deliveryStationsValidatedByGoogle.stream().allMatch(Boolean.TRUE::equals);
|
return deliveryStationsValidatedByGoogle.stream().allMatch(Boolean.TRUE::equals);
|
||||||
}
|
}
|
||||||
|
|
||||||
private RouteCalculationResult calculateRouteAcrossAllStations() {
|
private RouteCalculationBundle calculateRouteBundle() {
|
||||||
if (!pickupAddressValidatedByGoogle) {
|
if (!pickupAddressValidatedByGoogle) {
|
||||||
return createInvalidRouteResult("Die Abholstation ist nicht validiert.");
|
return createInvalidRouteBundle("Die Abholstation ist nicht validiert.");
|
||||||
}
|
}
|
||||||
|
|
||||||
List<AddressValidationResult> stationResults = new ArrayList<>();
|
|
||||||
AddressValidationResult pickupValidation = getOrValidatePickupAddressResult();
|
AddressValidationResult pickupValidation = getOrValidatePickupAddressResult();
|
||||||
if (pickupValidation == null || !pickupValidation.isValid()) {
|
if (pickupValidation == null || !pickupValidation.isValid()) {
|
||||||
return createInvalidRouteResult("Die Abholstation konnte nicht validiert werden.");
|
return createInvalidRouteBundle("Die Abholstation konnte nicht validiert werden.");
|
||||||
}
|
}
|
||||||
stationResults.add(pickupValidation);
|
|
||||||
|
List<AddressValidationResult> deliveryValidations = new ArrayList<>();
|
||||||
|
|
||||||
for (int i = 0; i < deliveryStationsState.size(); i++) {
|
for (int i = 0; i < deliveryStationsState.size(); i++) {
|
||||||
DeliveryStation station = deliveryStationsState.get(i);
|
DeliveryStation station = deliveryStationsState.get(i);
|
||||||
if (hasDeliveryStationValidationErrors(station)) {
|
if (hasDeliveryStationValidationErrors(station)) {
|
||||||
return createInvalidRouteResult("Nicht alle Lieferstationen sind vollständig ausgefüllt.");
|
return createInvalidRouteBundle("Nicht alle Lieferstationen sind vollständig ausgefüllt.");
|
||||||
}
|
}
|
||||||
if (i >= deliveryStationsValidatedByGoogle.size() || !Boolean.TRUE.equals(deliveryStationsValidatedByGoogle.get(i))) {
|
if (i >= deliveryStationsValidatedByGoogle.size()
|
||||||
return createInvalidRouteResult("Nicht alle Lieferstationen sind validiert.");
|
|| !Boolean.TRUE.equals(deliveryStationsValidatedByGoogle.get(i))) {
|
||||||
|
return createInvalidRouteBundle("Nicht alle Lieferstationen sind validiert.");
|
||||||
}
|
}
|
||||||
|
|
||||||
AddressValidationResult deliveryValidation = getOrValidateDeliveryAddressResult(i);
|
AddressValidationResult deliveryValidation = getOrValidateDeliveryAddressResult(i);
|
||||||
if (deliveryValidation == null || !deliveryValidation.isValid()) {
|
if (deliveryValidation == null || !deliveryValidation.isValid()) {
|
||||||
return createInvalidRouteResult(
|
return createInvalidRouteBundle(
|
||||||
String.format("Die Strecke konnte für Lieferstation %d nicht berechnet werden.", i + 1));
|
String.format("Die Strecke konnte für Lieferstation %d nicht berechnet werden.", i + 1));
|
||||||
}
|
}
|
||||||
stationResults.add(deliveryValidation);
|
deliveryValidations.add(deliveryValidation);
|
||||||
}
|
}
|
||||||
|
|
||||||
return addressValidationService.calculateRoute(stationResults);
|
Map<Integer, RouteCalculationResult> deliveryRoutes = new HashMap<>();
|
||||||
|
AddressValidationResult previousStation = pickupValidation;
|
||||||
|
for (int i = 0; i < deliveryValidations.size(); i++) {
|
||||||
|
AddressValidationResult currentStation = deliveryValidations.get(i);
|
||||||
|
RouteCalculationResult legRoute = addressValidationService.calculateRoute(previousStation, currentStation);
|
||||||
|
if (legRoute == null || !legRoute.isValid()) {
|
||||||
|
return createInvalidRouteBundle(
|
||||||
|
String.format("Die Strecke konnte für Lieferstation %d nicht berechnet werden.", i + 1));
|
||||||
|
}
|
||||||
|
deliveryRoutes.put(i, legRoute);
|
||||||
|
previousStation = currentStation;
|
||||||
|
}
|
||||||
|
|
||||||
|
RouteCalculationResult totalRoute = aggregateLegRoutes(deliveryRoutes, deliveryValidations.size());
|
||||||
|
return new RouteCalculationBundle(totalRoute, deliveryRoutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private RouteCalculationResult aggregateLegRoutes(Map<Integer, RouteCalculationResult> deliveryRoutes, int legCount) {
|
||||||
|
if (deliveryRoutes == null || deliveryRoutes.size() != legCount || legCount == 0) {
|
||||||
|
return createInvalidRouteResult("Die Gesamtstrecke konnte nicht berechnet werden.");
|
||||||
|
}
|
||||||
|
|
||||||
|
double totalDistanceKm = 0.0;
|
||||||
|
int totalDurationSeconds = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < legCount; i++) {
|
||||||
|
RouteCalculationResult legRoute = deliveryRoutes.get(i);
|
||||||
|
if (legRoute == null || !legRoute.isValid()) {
|
||||||
|
return createInvalidRouteResult("Die Gesamtstrecke konnte nicht berechnet werden.");
|
||||||
|
}
|
||||||
|
totalDistanceKm += legRoute.getDistanceKm();
|
||||||
|
totalDurationSeconds += legRoute.getDurationSeconds();
|
||||||
|
}
|
||||||
|
|
||||||
|
RouteCalculationResult totalRoute = new RouteCalculationResult();
|
||||||
|
totalRoute.setValid(true);
|
||||||
|
totalRoute.setDistanceKm(totalDistanceKm);
|
||||||
|
totalRoute.setDurationSeconds(totalDurationSeconds);
|
||||||
|
totalRoute.setFormattedDistance(String.format(Locale.GERMANY, "%.1f km", totalDistanceKm));
|
||||||
|
totalRoute.setFormattedDuration(totalRoute.getFormattedDurationLong());
|
||||||
|
totalRoute.setRouteMessage(
|
||||||
|
String.format("Route: %s, Dauer: %s", totalRoute.getFormattedDistance(),
|
||||||
|
totalRoute.getFormattedDurationLong()));
|
||||||
|
return totalRoute;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RouteCalculationResult createInvalidRouteResult(String message) {
|
||||||
|
RouteCalculationResult routeResult = new RouteCalculationResult();
|
||||||
|
routeResult.setRouteMessage(message);
|
||||||
|
return routeResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
private AddressValidationResult getOrValidatePickupAddressResult() {
|
private AddressValidationResult getOrValidatePickupAddressResult() {
|
||||||
@@ -2028,10 +2257,8 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
addressValidationResults.put(resultKey, validationResult);
|
addressValidationResults.put(resultKey, validationResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
private RouteCalculationResult createInvalidRouteResult(String message) {
|
private RouteCalculationBundle createInvalidRouteBundle(String message) {
|
||||||
RouteCalculationResult routeResult = new RouteCalculationResult();
|
return new RouteCalculationBundle(createInvalidRouteResult(message), Map.of());
|
||||||
routeResult.setRouteMessage(message);
|
|
||||||
return routeResult;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Dialog createRouteLoadingDialog() {
|
private Dialog createRouteLoadingDialog() {
|
||||||
@@ -2055,8 +2282,14 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
return dialog;
|
return dialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyCalculatedRoute(RouteCalculationResult routeResult) {
|
private void applyCalculatedRoutes(RouteCalculationBundle routeBundle) {
|
||||||
|
RouteCalculationResult routeResult = routeBundle.totalRoute();
|
||||||
routeCalculationResult = routeResult;
|
routeCalculationResult = routeResult;
|
||||||
|
pickupToDeliveryRouteResults.clear();
|
||||||
|
if (routeBundle.deliveryRoutes() != null) {
|
||||||
|
pickupToDeliveryRouteResults.putAll(routeBundle.deliveryRoutes());
|
||||||
|
}
|
||||||
|
renderDeliveryStationDistanceChips();
|
||||||
|
|
||||||
routeDistanceLabel.setText(routeResult.getFormattedDistance());
|
routeDistanceLabel.setText(routeResult.getFormattedDistance());
|
||||||
routeDurationLabel.setText(routeResult.getFormattedDurationLong());
|
routeDurationLabel.setText(routeResult.getFormattedDurationLong());
|
||||||
@@ -2103,6 +2336,25 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void renderDeliveryStationDistanceChips() {
|
||||||
|
if (deliveryStationDistanceChips.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < deliveryStationDistanceChips.size(); i++) {
|
||||||
|
Span chip = deliveryStationDistanceChips.get(i);
|
||||||
|
RouteCalculationResult stationRoute = pickupToDeliveryRouteResults.get(i);
|
||||||
|
if (stationRoute == null || !stationRoute.isValid()) {
|
||||||
|
chip.setText("");
|
||||||
|
chip.setVisible(false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
chip.setText(stationRoute.getFormattedDistance());
|
||||||
|
chip.setVisible(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registriert ValueChangeListener für alle Adressfelder, um bei Änderungen die
|
* Registriert ValueChangeListener für alle Adressfelder, um bei Änderungen die
|
||||||
* Streckeninformationen zurückzusetzen.
|
* Streckeninformationen zurückzusetzen.
|
||||||
@@ -2179,6 +2431,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
private void resetRouteInformation() {
|
private void resetRouteInformation() {
|
||||||
// Routenberechnung zurücksetzen
|
// Routenberechnung zurücksetzen
|
||||||
routeCalculationResult = null;
|
routeCalculationResult = null;
|
||||||
|
pickupToDeliveryRouteResults.clear();
|
||||||
|
|
||||||
// Validierungsergebnisse zurücksetzen
|
// Validierungsergebnisse zurücksetzen
|
||||||
addressValidationResults.clear();
|
addressValidationResults.clear();
|
||||||
@@ -2196,6 +2449,10 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
if (manualRouteInputBox != null) {
|
if (manualRouteInputBox != null) {
|
||||||
manualRouteInputBox.setVisible(true);
|
manualRouteInputBox.setVisible(true);
|
||||||
}
|
}
|
||||||
|
for (Span chip : deliveryStationDistanceChips) {
|
||||||
|
chip.setText("");
|
||||||
|
chip.setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
log.debug("Streckeninformationen zurückgesetzt aufgrund von Adressänderungen");
|
log.debug("Streckeninformationen zurückgesetzt aufgrund von Adressänderungen");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import com.vaadin.flow.router.BeforeEvent;
|
|||||||
import com.vaadin.flow.router.HasUrlParameter;
|
import com.vaadin.flow.router.HasUrlParameter;
|
||||||
import de.assecutor.votianlt.model.Customer;
|
import de.assecutor.votianlt.model.Customer;
|
||||||
import de.assecutor.votianlt.model.Job;
|
import de.assecutor.votianlt.model.Job;
|
||||||
|
import de.assecutor.votianlt.model.JobServiceSelection;
|
||||||
import de.assecutor.votianlt.model.Service;
|
import de.assecutor.votianlt.model.Service;
|
||||||
import de.assecutor.votianlt.model.User;
|
import de.assecutor.votianlt.model.User;
|
||||||
import de.assecutor.votianlt.model.InvoiceTemplate;
|
import de.assecutor.votianlt.model.InvoiceTemplate;
|
||||||
@@ -73,6 +74,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
*/
|
*/
|
||||||
public static class ServiceRow {
|
public static class ServiceRow {
|
||||||
private Service service;
|
private Service service;
|
||||||
|
private JobServiceSelection selection;
|
||||||
|
|
||||||
public ServiceRow() {
|
public ServiceRow() {
|
||||||
this.service = null;
|
this.service = null;
|
||||||
@@ -82,6 +84,11 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
this.service = service;
|
this.service = service;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ServiceRow(Service service, JobServiceSelection selection) {
|
||||||
|
this.service = service;
|
||||||
|
this.selection = selection;
|
||||||
|
}
|
||||||
|
|
||||||
public Service getService() {
|
public Service getService() {
|
||||||
return service;
|
return service;
|
||||||
}
|
}
|
||||||
@@ -90,6 +97,14 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
this.service = service;
|
this.service = service;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public JobServiceSelection getSelection() {
|
||||||
|
return selection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSelection(JobServiceSelection selection) {
|
||||||
|
this.selection = selection;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isEmpty() {
|
public boolean isEmpty() {
|
||||||
return service == null;
|
return service == null;
|
||||||
}
|
}
|
||||||
@@ -119,6 +134,19 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
* Lädt die Services, die beim Job-Erstellen ausgewählt wurden.
|
* Lädt die Services, die beim Job-Erstellen ausgewählt wurden.
|
||||||
*/
|
*/
|
||||||
private void loadSelectedServicesFromJob() {
|
private void loadSelectedServicesFromJob() {
|
||||||
|
if (currentJob.getSelectedServices() != null && !currentJob.getSelectedServices().isEmpty()) {
|
||||||
|
gridRows.clear();
|
||||||
|
for (JobServiceSelection selection : currentJob.getSelectedServices()) {
|
||||||
|
if (selection.getServiceId() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
serviceRepository.findById(selection.getServiceId()).ifPresent(service -> {
|
||||||
|
gridRows.add(new ServiceRow(service, selection));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (currentJob.getServiceIds() != null && !currentJob.getServiceIds().isEmpty()) {
|
if (currentJob.getServiceIds() != null && !currentJob.getServiceIds().isEmpty()) {
|
||||||
gridRows.clear();
|
gridRows.clear();
|
||||||
for (String serviceId : currentJob.getServiceIds()) {
|
for (String serviceId : currentJob.getServiceIds()) {
|
||||||
@@ -267,6 +295,9 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
return "";
|
return "";
|
||||||
}).setHeader(getTranslation("createinvoice.column.service")).setAutoWidth(true).setFlexGrow(2);
|
}).setHeader(getTranslation("createinvoice.column.service")).setAutoWidth(true).setFlexGrow(2);
|
||||||
|
|
||||||
|
servicesGrid.addColumn(this::getDeliveryStationLabel).setHeader(getTranslation("addjob.services.deliverystation"))
|
||||||
|
.setAutoWidth(true).setFlexGrow(1);
|
||||||
|
|
||||||
// Calculation basis column (read-only)
|
// Calculation basis column (read-only)
|
||||||
servicesGrid.addColumn(row -> {
|
servicesGrid.addColumn(row -> {
|
||||||
if (row.getService() != null && row.getService().getCalculationBasis() != null) {
|
if (row.getService() != null && row.getService().getCalculationBasis() != null) {
|
||||||
@@ -282,7 +313,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
// Price column (read-only)
|
// Price column (read-only)
|
||||||
servicesGrid.addColumn(row -> {
|
servicesGrid.addColumn(row -> {
|
||||||
if (row.getService() != null) {
|
if (row.getService() != null) {
|
||||||
BigDecimal price = calculateServicePrice(row.getService());
|
BigDecimal price = calculateServicePrice(row);
|
||||||
if (price != null) {
|
if (price != null) {
|
||||||
return price.setScale(2, RoundingMode.HALF_UP) + " €";
|
return price.setScale(2, RoundingMode.HALF_UP) + " €";
|
||||||
}
|
}
|
||||||
@@ -296,8 +327,8 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
return servicesSection;
|
return servicesSection;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Service> getSelectedServices() {
|
private List<ServiceRow> getSelectedServices() {
|
||||||
return gridRows.stream().filter(row -> row.getService() != null).map(ServiceRow::getService).toList();
|
return gridRows.stream().filter(row -> row.getService() != null).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Div createSummarySection() {
|
private Div createSummarySection() {
|
||||||
@@ -311,7 +342,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
|
|
||||||
// Calculate totals
|
// Calculate totals
|
||||||
BigDecimal netAmount = calculateNetAmount();
|
BigDecimal netAmount = calculateNetAmount();
|
||||||
BigDecimal vatRate = calculateAverageVatRate();
|
BigDecimal vatRate = Service.FIXED_VAT_RATE;
|
||||||
BigDecimal vatAmount = netAmount.multiply(vatRate);
|
BigDecimal vatAmount = netAmount.multiply(vatRate);
|
||||||
BigDecimal totalAmount = netAmount.add(vatAmount);
|
BigDecimal totalAmount = netAmount.add(vatAmount);
|
||||||
|
|
||||||
@@ -320,10 +351,6 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
|
|
||||||
priceTable.add(createPriceRow(getTranslation("createinvoice.summary.net") + ":",
|
priceTable.add(createPriceRow(getTranslation("createinvoice.summary.net") + ":",
|
||||||
netAmount.setScale(2, RoundingMode.HALF_UP) + " €", false));
|
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") + ":",
|
priceTable.add(createPriceRow(getTranslation("createinvoice.summary.total") + ":",
|
||||||
totalAmount.setScale(2, RoundingMode.HALF_UP) + " €", true));
|
totalAmount.setScale(2, RoundingMode.HALF_UP) + " €", true));
|
||||||
|
|
||||||
@@ -349,7 +376,17 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
private BigDecimal calculateServicePrice(Service service) {
|
private String getDeliveryStationLabel(ServiceRow row) {
|
||||||
|
JobServiceSelection selection = row.getSelection();
|
||||||
|
if (selection == null || selection.getDeliveryStationOrder() == null) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
return "Lieferstation " + (selection.getDeliveryStationOrder() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal calculateServicePrice(ServiceRow row) {
|
||||||
|
Service service = row.getService();
|
||||||
|
JobServiceSelection selection = row.getSelection();
|
||||||
if (service.getCalculationBasis() == null) {
|
if (service.getCalculationBasis() == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -357,32 +394,55 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
if (service.getCalculationBasis() == Service.CalculationBasis.FLAT_RATE && service.getPrice() != null) {
|
if (service.getCalculationBasis() == Service.CalculationBasis.FLAT_RATE && service.getPrice() != null) {
|
||||||
return service.getPrice();
|
return service.getPrice();
|
||||||
} else if (service.getCalculationBasis() == Service.CalculationBasis.DISTANCE
|
} else if (service.getCalculationBasis() == Service.CalculationBasis.DISTANCE
|
||||||
&& service.getPricePerKilometer() != null && currentJob.getRouteDistanceKm() != null) {
|
&& service.getPricePerKilometer() != null && getRouteDistanceKm(selection) != null) {
|
||||||
BigDecimal kilometers = BigDecimal.valueOf(currentJob.getRouteDistanceKm());
|
BigDecimal kilometers = BigDecimal.valueOf(getRouteDistanceKm(selection));
|
||||||
return service.getPricePerKilometer().multiply(kilometers);
|
return service.getPricePerKilometer().multiply(kilometers);
|
||||||
} else if (service.getCalculationBasis() == Service.CalculationBasis.TIME
|
} else if (service.getCalculationBasis() == Service.CalculationBasis.TIME
|
||||||
&& service.getPricePer15Minutes() != null && currentJob.getTimeIn15MinUnits() != null) {
|
&& service.getPricePer15Minutes() != null && getTimeIn15MinUnits(selection) != null) {
|
||||||
BigDecimal timeUnits = new BigDecimal(currentJob.getTimeIn15MinUnits());
|
BigDecimal timeUnits = new BigDecimal(getTimeIn15MinUnits(selection));
|
||||||
return service.getPricePer15Minutes().multiply(timeUnits);
|
return service.getPricePer15Minutes().multiply(timeUnits);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Double getRouteDistanceKm(JobServiceSelection selection) {
|
||||||
|
if (selection != null && selection.getRouteDistanceKm() != null) {
|
||||||
|
return selection.getRouteDistanceKm();
|
||||||
|
}
|
||||||
|
return currentJob.getRouteDistanceKm();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer getTimeIn15MinUnits(JobServiceSelection selection) {
|
||||||
|
Integer durationSeconds = selection != null && selection.getRouteDurationSeconds() != null
|
||||||
|
? selection.getRouteDurationSeconds()
|
||||||
|
: currentJob.getRouteDurationSeconds();
|
||||||
|
if (durationSeconds == null || durationSeconds <= 0) {
|
||||||
|
return currentJob.getTimeIn15MinUnits();
|
||||||
|
}
|
||||||
|
|
||||||
|
int units = durationSeconds / 900;
|
||||||
|
if (durationSeconds % 900 > 0) {
|
||||||
|
units++;
|
||||||
|
}
|
||||||
|
return units;
|
||||||
|
}
|
||||||
|
|
||||||
private BigDecimal calculateNetAmount() {
|
private BigDecimal calculateNetAmount() {
|
||||||
BigDecimal total = BigDecimal.ZERO;
|
BigDecimal total = BigDecimal.ZERO;
|
||||||
|
|
||||||
for (Service service : getSelectedServices()) {
|
for (ServiceRow row : getSelectedServices()) {
|
||||||
|
Service service = row.getService();
|
||||||
if (service.getCalculationBasis() == Service.CalculationBasis.FLAT_RATE && service.getPrice() != null) {
|
if (service.getCalculationBasis() == Service.CalculationBasis.FLAT_RATE && service.getPrice() != null) {
|
||||||
total = total.add(service.getPrice());
|
total = total.add(service.getPrice());
|
||||||
} else if (service.getCalculationBasis() == Service.CalculationBasis.DISTANCE
|
} else if (service.getCalculationBasis() == Service.CalculationBasis.DISTANCE
|
||||||
&& service.getPricePerKilometer() != null && currentJob.getRouteDistanceKm() != null) {
|
&& service.getPricePerKilometer() != null && getRouteDistanceKm(row.getSelection()) != null) {
|
||||||
BigDecimal kilometers = BigDecimal.valueOf(currentJob.getRouteDistanceKm());
|
BigDecimal kilometers = BigDecimal.valueOf(getRouteDistanceKm(row.getSelection()));
|
||||||
BigDecimal serviceTotal = service.getPricePerKilometer().multiply(kilometers);
|
BigDecimal serviceTotal = service.getPricePerKilometer().multiply(kilometers);
|
||||||
total = total.add(serviceTotal);
|
total = total.add(serviceTotal);
|
||||||
} else if (service.getCalculationBasis() == Service.CalculationBasis.TIME
|
} else if (service.getCalculationBasis() == Service.CalculationBasis.TIME
|
||||||
&& service.getPricePer15Minutes() != null && currentJob.getTimeIn15MinUnits() != null) {
|
&& service.getPricePer15Minutes() != null && getTimeIn15MinUnits(row.getSelection()) != null) {
|
||||||
BigDecimal timeUnits = new BigDecimal(currentJob.getTimeIn15MinUnits());
|
BigDecimal timeUnits = new BigDecimal(getTimeIn15MinUnits(row.getSelection()));
|
||||||
BigDecimal serviceTotal = service.getPricePer15Minutes().multiply(timeUnits);
|
BigDecimal serviceTotal = service.getPricePer15Minutes().multiply(timeUnits);
|
||||||
total = total.add(serviceTotal);
|
total = total.add(serviceTotal);
|
||||||
}
|
}
|
||||||
@@ -391,29 +451,6 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
private BigDecimal calculateAverageVatRate() {
|
|
||||||
List<Service> selectedServicesList = getSelectedServices();
|
|
||||||
if (selectedServicesList.isEmpty()) {
|
|
||||||
return new BigDecimal("0.19"); // Default 19% VAT
|
|
||||||
}
|
|
||||||
|
|
||||||
BigDecimal totalVat = BigDecimal.ZERO;
|
|
||||||
int count = 0;
|
|
||||||
|
|
||||||
for (Service service : selectedServicesList) {
|
|
||||||
if (service.getVatRate() != null) {
|
|
||||||
totalVat = totalVat.add(service.getVatRate());
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (count > 0) {
|
|
||||||
return totalVat.divide(new BigDecimal(count), 4, RoundingMode.HALF_UP);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new BigDecimal("0.19"); // Default 19% VAT
|
|
||||||
}
|
|
||||||
|
|
||||||
private String extractCompanyName(String customerSelection) {
|
private String extractCompanyName(String customerSelection) {
|
||||||
if (customerSelection == null || customerSelection.isBlank()) {
|
if (customerSelection == null || customerSelection.isBlank()) {
|
||||||
return "";
|
return "";
|
||||||
@@ -478,7 +515,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
String invoiceNumber = userInvoiceDataService.generateNextInvoiceNumber(user.getId());
|
String invoiceNumber = userInvoiceDataService.generateNextInvoiceNumber(user.getId());
|
||||||
|
|
||||||
BigDecimal netAmount = calculateNetAmount();
|
BigDecimal netAmount = calculateNetAmount();
|
||||||
BigDecimal vatRate = calculateAverageVatRate();
|
BigDecimal vatRate = Service.FIXED_VAT_RATE;
|
||||||
BigDecimal vatAmount = netAmount.multiply(vatRate);
|
BigDecimal vatAmount = netAmount.multiply(vatRate);
|
||||||
BigDecimal totalAmount = netAmount.add(vatAmount);
|
BigDecimal totalAmount = netAmount.add(vatAmount);
|
||||||
|
|
||||||
@@ -517,7 +554,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
throws Exception {
|
throws Exception {
|
||||||
// Calculate totals
|
// Calculate totals
|
||||||
BigDecimal netAmount = calculateNetAmount();
|
BigDecimal netAmount = calculateNetAmount();
|
||||||
BigDecimal vatRate = calculateAverageVatRate();
|
BigDecimal vatRate = Service.FIXED_VAT_RATE;
|
||||||
BigDecimal vatAmount = netAmount.multiply(vatRate);
|
BigDecimal vatAmount = netAmount.multiply(vatRate);
|
||||||
BigDecimal totalAmount = netAmount.add(vatAmount);
|
BigDecimal totalAmount = netAmount.add(vatAmount);
|
||||||
|
|
||||||
@@ -586,26 +623,19 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
|
|
||||||
// Services data - add as JSON array for the template
|
// Services data - add as JSON array for the template
|
||||||
List<Map<String, String>> servicesData = new ArrayList<>();
|
List<Map<String, String>> servicesData = new ArrayList<>();
|
||||||
for (Service service : getSelectedServices()) {
|
for (ServiceRow row : getSelectedServices()) {
|
||||||
|
Service service = row.getService();
|
||||||
Map<String, String> serviceData = new HashMap<>();
|
Map<String, String> serviceData = new HashMap<>();
|
||||||
serviceData.put("name", service.getName());
|
serviceData.put("name", service.getName());
|
||||||
|
|
||||||
// Calculate price based on calculation basis
|
// Calculate price based on calculation basis
|
||||||
BigDecimal price = calculateServicePrice(service);
|
BigDecimal price = calculateServicePrice(row);
|
||||||
if (price != null) {
|
if (price != null) {
|
||||||
serviceData.put("netAmount", price.setScale(2, RoundingMode.HALF_UP).toString().replace(".", ","));
|
serviceData.put("netAmount", price.setScale(2, RoundingMode.HALF_UP).toString().replace(".", ","));
|
||||||
} else {
|
} else {
|
||||||
serviceData.put("netAmount", "0,00");
|
serviceData.put("netAmount", "0,00");
|
||||||
}
|
}
|
||||||
|
|
||||||
// VAT rate
|
|
||||||
if (service.getVatRate() != null) {
|
|
||||||
serviceData.put("vatRate",
|
|
||||||
service.getVatRate().multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP) + "%");
|
|
||||||
} else {
|
|
||||||
serviceData.put("vatRate", "19%");
|
|
||||||
}
|
|
||||||
|
|
||||||
servicesData.add(serviceData);
|
servicesData.add(serviceData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -711,4 +741,4 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
public String getPageTitle() {
|
public String getPageTitle() {
|
||||||
return getTranslation("page.title.invoice.create");
|
return getTranslation("page.title.invoice.create");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -318,6 +318,7 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
|||||||
bankNameField = new TextField();
|
bankNameField = new TextField();
|
||||||
ibanField = new TextField();
|
ibanField = new TextField();
|
||||||
taxRateField = new TextField();
|
taxRateField = new TextField();
|
||||||
|
taxRateField.setValue("19");
|
||||||
introTextArea = new TextArea();
|
introTextArea = new TextArea();
|
||||||
termsTextArea = new TextArea();
|
termsTextArea = new TextArea();
|
||||||
pdfFrame = new IFrame();
|
pdfFrame = new IFrame();
|
||||||
@@ -646,8 +647,6 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
|||||||
bankNameField.setEnabled(enabled);
|
bankNameField.setEnabled(enabled);
|
||||||
if (ibanField != null)
|
if (ibanField != null)
|
||||||
ibanField.setEnabled(enabled);
|
ibanField.setEnabled(enabled);
|
||||||
if (taxRateField != null)
|
|
||||||
taxRateField.setEnabled(enabled);
|
|
||||||
if (introTextArea != null)
|
if (introTextArea != null)
|
||||||
introTextArea.setEnabled(enabled);
|
introTextArea.setEnabled(enabled);
|
||||||
if (termsTextArea != null)
|
if (termsTextArea != null)
|
||||||
@@ -710,7 +709,7 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
|||||||
|
|
||||||
// Create sample invoice items for preview
|
// Create sample invoice items for preview
|
||||||
List<CustomerInvoiceItem> items = new ArrayList<>();
|
List<CustomerInvoiceItem> items = new ArrayList<>();
|
||||||
BigDecimal vatRate = parseVatRate(safe(taxRateField));
|
BigDecimal vatRate = Service.FIXED_VAT_RATE;
|
||||||
|
|
||||||
CustomerInvoiceItem item1 = new CustomerInvoiceItem(new BigDecimal("1"), "Stk.",
|
CustomerInvoiceItem item1 = new CustomerInvoiceItem(new BigDecimal("1"), "Stk.",
|
||||||
"Beispiel-Dienstleistung 1", new BigDecimal("100.00"), vatRate);
|
"Beispiel-Dienstleistung 1", new BigDecimal("100.00"), vatRate);
|
||||||
@@ -752,24 +751,6 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private BigDecimal parseVatRate(String taxRateStr) {
|
|
||||||
try {
|
|
||||||
if (taxRateStr == null || taxRateStr.isEmpty()) {
|
|
||||||
return new BigDecimal("0.19"); // Default 19%
|
|
||||||
}
|
|
||||||
// Remove % sign if present and convert to decimal
|
|
||||||
taxRateStr = taxRateStr.replace("%", "").trim();
|
|
||||||
BigDecimal rate = new BigDecimal(taxRateStr);
|
|
||||||
// If value is greater than 1, assume it's a percentage and convert
|
|
||||||
if (rate.compareTo(BigDecimal.ONE) > 0) {
|
|
||||||
rate = rate.divide(new BigDecimal("100"));
|
|
||||||
}
|
|
||||||
return rate;
|
|
||||||
} catch (Exception e) {
|
|
||||||
return new BigDecimal("0.19"); // Default 19% on error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utility: safe getter für TextField/TextArea
|
// Utility: safe getter für TextField/TextArea
|
||||||
private String safe(TextField f) {
|
private String safe(TextField f) {
|
||||||
return f != null && f.getValue() != null ? f.getValue() : "";
|
return f != null && f.getValue() != null ? f.getValue() : "";
|
||||||
@@ -789,7 +770,7 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
|||||||
taxNumberField.setValue(safe(currentInvoiceData.getTaxNumber()));
|
taxNumberField.setValue(safe(currentInvoiceData.getTaxNumber()));
|
||||||
bankNameField.setValue(safe(currentInvoiceData.getBankName()));
|
bankNameField.setValue(safe(currentInvoiceData.getBankName()));
|
||||||
ibanField.setValue(safe(currentInvoiceData.getIban()));
|
ibanField.setValue(safe(currentInvoiceData.getIban()));
|
||||||
taxRateField.setValue(safe(currentInvoiceData.getTaxRate()));
|
taxRateField.setValue("19");
|
||||||
introTextArea.setValue(safe(currentInvoiceData.getIntroText()));
|
introTextArea.setValue(safe(currentInvoiceData.getIntroText()));
|
||||||
termsTextArea.setValue(safe(currentInvoiceData.getPaymentTerms()));
|
termsTextArea.setValue(safe(currentInvoiceData.getPaymentTerms()));
|
||||||
|
|
||||||
@@ -804,7 +785,7 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
|||||||
private void saveInvoiceData() {
|
private void saveInvoiceData() {
|
||||||
currentInvoiceData = userInvoiceDataService.createOrUpdate(currentUser.getId(), billingEnabled.getValue(),
|
currentInvoiceData = userInvoiceDataService.createOrUpdate(currentUser.getId(), billingEnabled.getValue(),
|
||||||
prefixField.getValue(), ustIdField.getValue(), taxNumberField.getValue(), bankNameField.getValue(),
|
prefixField.getValue(), ustIdField.getValue(), taxNumberField.getValue(), bankNameField.getValue(),
|
||||||
ibanField.getValue(), taxRateField.getValue(), introTextArea.getValue(), termsTextArea.getValue());
|
ibanField.getValue(), "19", introTextArea.getValue(), termsTextArea.getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
private String safe(String value) {
|
private String safe(String value) {
|
||||||
@@ -979,8 +960,6 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
|||||||
VaadinIcon.LIST, "services.list", "Artikel 1: 100,00 €\nArtikel 2: 50,00 €");
|
VaadinIcon.LIST, "services.list", "Artikel 1: 100,00 €\nArtikel 2: 50,00 €");
|
||||||
Div servicesNetBlock = createServicesVariableTemplate(getTranslation("profile.invoice.net"),
|
Div servicesNetBlock = createServicesVariableTemplate(getTranslation("profile.invoice.net"),
|
||||||
VaadinIcon.COIN_PILES, "services.net_total", "150,00 €");
|
VaadinIcon.COIN_PILES, "services.net_total", "150,00 €");
|
||||||
Div servicesVatBlock = createServicesVariableTemplate(getTranslation("profile.invoice.vat"),
|
|
||||||
VaadinIcon.COIN_PILES, "services.vat_total", "28,50 €");
|
|
||||||
Div servicesGrossBlock = createServicesVariableTemplate(getTranslation("profile.invoice.gross"),
|
Div servicesGrossBlock = createServicesVariableTemplate(getTranslation("profile.invoice.gross"),
|
||||||
VaadinIcon.MONEY, "services.gross_total", "178,50 €");
|
VaadinIcon.MONEY, "services.gross_total", "178,50 €");
|
||||||
|
|
||||||
@@ -1027,8 +1006,8 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
|||||||
"image");
|
"image");
|
||||||
|
|
||||||
panel.add(invoiceHeader, senderCompany, senderName, senderAddress, senderCity, senderEmail, senderPhone,
|
panel.add(invoiceHeader, senderCompany, senderName, senderAddress, senderCity, senderEmail, senderPhone,
|
||||||
invoiceNumber, servicesHeader, servicesListBlock, servicesNetBlock, servicesVatBlock,
|
invoiceNumber, servicesHeader, servicesListBlock, servicesNetBlock, servicesGrossBlock, customerHeader,
|
||||||
servicesGrossBlock, customerHeader, customerCompany, customerName, customerAddress, customerCity,
|
customerCompany, customerName, customerAddress, customerCity,
|
||||||
customerEmail, customerPhone, freeHeader, textBlock, headerBlock, dateBlock, customerBlock,
|
customerEmail, customerPhone, freeHeader, textBlock, headerBlock, dateBlock, customerBlock,
|
||||||
companyBlock, amountBlock, lineBlock, imageBlock);
|
companyBlock, amountBlock, lineBlock, imageBlock);
|
||||||
|
|
||||||
@@ -1476,7 +1455,6 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
|||||||
case "customer.phone" -> "Telefon des Kunden";
|
case "customer.phone" -> "Telefon des Kunden";
|
||||||
case "services.list" -> "Liste aller Leistungen auf der Rechnung";
|
case "services.list" -> "Liste aller Leistungen auf der Rechnung";
|
||||||
case "services.net_total" -> "Nettosumme aller Leistungen";
|
case "services.net_total" -> "Nettosumme aller Leistungen";
|
||||||
case "services.vat_total" -> "Umsatzsteuer aller Leistungen";
|
|
||||||
case "services.gross_total" -> "Bruttosumme aller Leistungen";
|
case "services.gross_total" -> "Bruttosumme aller Leistungen";
|
||||||
default -> "Variable: " + variable;
|
default -> "Variable: " + variable;
|
||||||
};
|
};
|
||||||
@@ -1530,13 +1508,6 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
|||||||
return getTranslation("profile.services.calculated");
|
return getTranslation("profile.services.calculated");
|
||||||
}).setHeader(getTranslation("common.price")).setSortable(true);
|
}).setHeader(getTranslation("common.price")).setSortable(true);
|
||||||
|
|
||||||
servicesGrid.addColumn(service -> {
|
|
||||||
if (service.getVatRate() != null) {
|
|
||||||
return service.getVatRate().multiply(new BigDecimal("100")) + " %";
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}).setHeader(getTranslation("profile.services.vatrate")).setSortable(true);
|
|
||||||
|
|
||||||
servicesGrid
|
servicesGrid
|
||||||
.addColumn(
|
.addColumn(
|
||||||
service -> service.isMandatory() ? getTranslation("common.yes") : getTranslation("common.no"))
|
service -> service.isMandatory() ? getTranslation("common.yes") : getTranslation("common.no"))
|
||||||
@@ -1626,16 +1597,6 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
|||||||
calculationBasisCombo.setRequired(true);
|
calculationBasisCombo.setRequired(true);
|
||||||
calculationBasisCombo.setRequiredIndicatorVisible(true);
|
calculationBasisCombo.setRequiredIndicatorVisible(true);
|
||||||
|
|
||||||
// VAT rate field
|
|
||||||
NumberField vatRateField = new NumberField(getTranslation("profile.services.vatrate.percent"));
|
|
||||||
vatRateField.setWidthFull();
|
|
||||||
vatRateField.setMin(0);
|
|
||||||
vatRateField.setMax(100);
|
|
||||||
vatRateField.setStep(0.1);
|
|
||||||
vatRateField.setValue(19.0); // Default 19%
|
|
||||||
vatRateField.setRequired(true);
|
|
||||||
vatRateField.setRequiredIndicatorVisible(true);
|
|
||||||
|
|
||||||
// Mandatory checkbox
|
// Mandatory checkbox
|
||||||
Checkbox mandatoryCheckbox = new Checkbox(getTranslation("profile.services.mandatory"));
|
Checkbox mandatoryCheckbox = new Checkbox(getTranslation("profile.services.mandatory"));
|
||||||
mandatoryCheckbox.setValue(false);
|
mandatoryCheckbox.setValue(false);
|
||||||
@@ -1644,9 +1605,6 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
|||||||
if (service != null) {
|
if (service != null) {
|
||||||
nameField.setValue(service.getName());
|
nameField.setValue(service.getName());
|
||||||
calculationBasisCombo.setValue(service.getCalculationBasis());
|
calculationBasisCombo.setValue(service.getCalculationBasis());
|
||||||
if (service.getVatRate() != null) {
|
|
||||||
vatRateField.setValue(service.getVatRate().multiply(new BigDecimal("100")).doubleValue());
|
|
||||||
}
|
|
||||||
mandatoryCheckbox.setValue(service.isMandatory());
|
mandatoryCheckbox.setValue(service.isMandatory());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1679,9 +1637,6 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
|||||||
if (service != null) {
|
if (service != null) {
|
||||||
nameField.setValue(service.getName());
|
nameField.setValue(service.getName());
|
||||||
calculationBasisCombo.setValue(service.getCalculationBasis());
|
calculationBasisCombo.setValue(service.getCalculationBasis());
|
||||||
if (service.getVatRate() != null) {
|
|
||||||
vatRateField.setValue(service.getVatRate().multiply(new BigDecimal("100")).doubleValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the appropriate price field based on calculation basis
|
// Set the appropriate price field based on calculation basis
|
||||||
if (service.getCalculationBasis() != null) {
|
if (service.getCalculationBasis() != null) {
|
||||||
@@ -1721,7 +1676,7 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
|||||||
timePriceField.setVisible(initialBasis == Service.CalculationBasis.TIME);
|
timePriceField.setVisible(initialBasis == Service.CalculationBasis.TIME);
|
||||||
|
|
||||||
formLayout.add(nameField, calculationBasisCombo, flatRatePriceField, distancePriceField, timePriceField,
|
formLayout.add(nameField, calculationBasisCombo, flatRatePriceField, distancePriceField, timePriceField,
|
||||||
vatRateField, mandatoryCheckbox);
|
mandatoryCheckbox);
|
||||||
|
|
||||||
// Action buttons
|
// Action buttons
|
||||||
HorizontalLayout buttonLayout = new HorizontalLayout();
|
HorizontalLayout buttonLayout = new HorizontalLayout();
|
||||||
@@ -1734,7 +1689,7 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
|||||||
|
|
||||||
Button saveButton = new Button(getTranslation("button.savechanges"), e -> {
|
Button saveButton = new Button(getTranslation("button.savechanges"), e -> {
|
||||||
if (validateServiceForm(nameField, calculationBasisCombo, flatRatePriceField, distancePriceField,
|
if (validateServiceForm(nameField, calculationBasisCombo, flatRatePriceField, distancePriceField,
|
||||||
timePriceField, vatRateField, mandatoryCheckbox)) {
|
timePriceField, mandatoryCheckbox)) {
|
||||||
// Get the appropriate price based on calculation basis
|
// Get the appropriate price based on calculation basis
|
||||||
BigDecimal priceValue = BigDecimal.ZERO;
|
BigDecimal priceValue = BigDecimal.ZERO;
|
||||||
Service.CalculationBasis selectedBasis = calculationBasisCombo.getValue();
|
Service.CalculationBasis selectedBasis = calculationBasisCombo.getValue();
|
||||||
@@ -1748,10 +1703,8 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
|||||||
priceValue = new BigDecimal(timePriceField.getValue());
|
priceValue = new BigDecimal(timePriceField.getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
BigDecimal vatRate = new BigDecimal(vatRateField.getValue()).divide(new BigDecimal("100"));
|
|
||||||
boolean mandatory = mandatoryCheckbox.getValue();
|
boolean mandatory = mandatoryCheckbox.getValue();
|
||||||
saveService(service, nameField.getValue(), calculationBasisCombo.getValue(), priceValue, vatRate,
|
saveService(service, nameField.getValue(), calculationBasisCombo.getValue(), priceValue, mandatory);
|
||||||
mandatory);
|
|
||||||
dialog.close();
|
dialog.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1769,7 +1722,7 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
|||||||
*/
|
*/
|
||||||
private boolean validateServiceForm(TextField nameField, ComboBox<Service.CalculationBasis> calculationBasisCombo,
|
private boolean validateServiceForm(TextField nameField, ComboBox<Service.CalculationBasis> calculationBasisCombo,
|
||||||
NumberField flatRatePriceField, NumberField distancePriceField, NumberField timePriceField,
|
NumberField flatRatePriceField, NumberField distancePriceField, NumberField timePriceField,
|
||||||
NumberField vatRateField, Checkbox mandatoryCheckbox) {
|
Checkbox mandatoryCheckbox) {
|
||||||
boolean isValid = true;
|
boolean isValid = true;
|
||||||
|
|
||||||
if (nameField.isEmpty()) {
|
if (nameField.isEmpty()) {
|
||||||
@@ -1816,14 +1769,6 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (vatRateField.isEmpty() || vatRateField.getValue() == null) {
|
|
||||||
vatRateField.setInvalid(true);
|
|
||||||
vatRateField.setErrorMessage(getTranslation("profile.services.validation.vatrate"));
|
|
||||||
isValid = false;
|
|
||||||
} else {
|
|
||||||
vatRateField.setInvalid(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return isValid;
|
return isValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1831,14 +1776,14 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
|||||||
* Save service to database
|
* Save service to database
|
||||||
*/
|
*/
|
||||||
private void saveService(Service existingService, String name, Service.CalculationBasis calculationBasis,
|
private void saveService(Service existingService, String name, Service.CalculationBasis calculationBasis,
|
||||||
BigDecimal priceValue, BigDecimal vatRate, boolean mandatory) {
|
BigDecimal priceValue, boolean mandatory) {
|
||||||
try {
|
try {
|
||||||
Service service;
|
Service service;
|
||||||
if (existingService != null) {
|
if (existingService != null) {
|
||||||
service = existingService;
|
service = existingService;
|
||||||
service.setName(name);
|
service.setName(name);
|
||||||
service.setCalculationBasis(calculationBasis);
|
service.setCalculationBasis(calculationBasis);
|
||||||
service.setVatRate(vatRate);
|
service.setVatRate(Service.FIXED_VAT_RATE);
|
||||||
service.setMandatory(mandatory);
|
service.setMandatory(mandatory);
|
||||||
|
|
||||||
// Set the appropriate price field based on calculation basis
|
// Set the appropriate price field based on calculation basis
|
||||||
@@ -1860,8 +1805,8 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
service = new Service(currentUser.getId().toString(), name, calculationBasis, priceValue, vatRate,
|
service = new Service(currentUser.getId().toString(), name, calculationBasis, priceValue,
|
||||||
mandatory);
|
Service.FIXED_VAT_RATE, mandatory);
|
||||||
}
|
}
|
||||||
|
|
||||||
serviceRepository.save(service);
|
serviceRepository.save(service);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import com.vaadin.flow.theme.lumo.LumoUtility;
|
|||||||
import de.assecutor.votianlt.model.CargoItem;
|
import de.assecutor.votianlt.model.CargoItem;
|
||||||
import de.assecutor.votianlt.model.DeliveryStation;
|
import de.assecutor.votianlt.model.DeliveryStation;
|
||||||
import de.assecutor.votianlt.model.Job;
|
import de.assecutor.votianlt.model.Job;
|
||||||
|
import de.assecutor.votianlt.model.JobServiceSelection;
|
||||||
import de.assecutor.votianlt.model.LocationPosition;
|
import de.assecutor.votianlt.model.LocationPosition;
|
||||||
import de.assecutor.votianlt.model.task.BaseTask;
|
import de.assecutor.votianlt.model.task.BaseTask;
|
||||||
import de.assecutor.votianlt.model.task.TodoListTask;
|
import de.assecutor.votianlt.model.task.TodoListTask;
|
||||||
@@ -223,8 +224,6 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
|||||||
|
|
||||||
priceTable.add(createPriceRow(getTranslation("jobsummary.info.netto") + ":",
|
priceTable.add(createPriceRow(getTranslation("jobsummary.info.netto") + ":",
|
||||||
formatPrice(priceResult.netAmount()), false));
|
formatPrice(priceResult.netAmount()), false));
|
||||||
priceTable.add(createPriceRow(getTranslation("jobsummary.info.ust") + ":", formatPrice(priceResult.vatAmount()),
|
|
||||||
false));
|
|
||||||
priceTable.add(createPriceRow(getTranslation("jobsummary.info.gesamt") + ":",
|
priceTable.add(createPriceRow(getTranslation("jobsummary.info.gesamt") + ":",
|
||||||
formatPrice(priceResult.totalAmount()), true));
|
formatPrice(priceResult.totalAmount()), true));
|
||||||
|
|
||||||
@@ -1414,6 +1413,30 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
|||||||
BigDecimal netTotal = BigDecimal.ZERO;
|
BigDecimal netTotal = BigDecimal.ZERO;
|
||||||
BigDecimal vatTotal = BigDecimal.ZERO;
|
BigDecimal vatTotal = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
List<JobServiceSelection> selectedServices = job.getSelectedServices();
|
||||||
|
if (selectedServices != null && !selectedServices.isEmpty()) {
|
||||||
|
for (JobServiceSelection selectedService : selectedServices) {
|
||||||
|
if (selectedService.getServiceId() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Service service = serviceRepository.findById(selectedService.getServiceId()).orElse(null);
|
||||||
|
if (service == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal price = calculateServicePrice(service, selectedService.getRouteDistanceKm(),
|
||||||
|
selectedService.getRouteDurationSeconds());
|
||||||
|
BigDecimal vatRate = Service.FIXED_VAT_RATE;
|
||||||
|
|
||||||
|
netTotal = netTotal.add(price);
|
||||||
|
vatTotal = vatTotal.add(price.multiply(vatRate));
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal totalAmount = netTotal.add(vatTotal);
|
||||||
|
return new PriceCalculationResult(netTotal, vatTotal, totalAmount);
|
||||||
|
}
|
||||||
|
|
||||||
List<String> serviceIds = job.getServiceIds();
|
List<String> serviceIds = job.getServiceIds();
|
||||||
if (serviceIds == null || serviceIds.isEmpty()) {
|
if (serviceIds == null || serviceIds.isEmpty()) {
|
||||||
// Fallback auf gespeicherten Preis
|
// Fallback auf gespeicherten Preis
|
||||||
@@ -1431,7 +1454,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
BigDecimal price = calculateServicePrice(service, routeDistance, durationSeconds);
|
BigDecimal price = calculateServicePrice(service, routeDistance, durationSeconds);
|
||||||
BigDecimal vatRate = service.getVatRate() != null ? service.getVatRate() : new BigDecimal("0.19");
|
BigDecimal vatRate = Service.FIXED_VAT_RATE;
|
||||||
|
|
||||||
netTotal = netTotal.add(price);
|
netTotal = netTotal.add(price);
|
||||||
vatTotal = vatTotal.add(price.multiply(vatRate));
|
vatTotal = vatTotal.add(price.multiply(vatRate));
|
||||||
|
|||||||
@@ -481,7 +481,6 @@ public class CustomerInvoiceService {
|
|||||||
|
|
||||||
// Calculate totals
|
// Calculate totals
|
||||||
double netTotal = 655.00;
|
double netTotal = 655.00;
|
||||||
double vatTotal = 124.45;
|
|
||||||
double grossTotal = 779.45;
|
double grossTotal = 779.45;
|
||||||
|
|
||||||
// Wrapper div
|
// Wrapper div
|
||||||
@@ -530,14 +529,6 @@ public class CustomerInvoiceService {
|
|||||||
.append(String.format(java.util.Locale.GERMANY, "%,.2f €", netTotal)).append("</td>");
|
.append(String.format(java.util.Locale.GERMANY, "%,.2f €", netTotal)).append("</td>");
|
||||||
html.append("</tr>");
|
html.append("</tr>");
|
||||||
|
|
||||||
// Umsatzsteuer - label in col 2, value in col 3
|
|
||||||
html.append("<tr>");
|
|
||||||
html.append("<td style='width:55%;padding:2px 0;'></td>");
|
|
||||||
html.append("<td style='width:20%;text-align:left;padding:2px 8px;white-space:nowrap;'>zzgl. 19% USt:</td>");
|
|
||||||
html.append("<td style='width:25%;text-align:right;padding:2px 8px;white-space:nowrap;font-weight:bold;'>")
|
|
||||||
.append(String.format(java.util.Locale.GERMANY, "%,.2f €", vatTotal)).append("</td>");
|
|
||||||
html.append("</tr>");
|
|
||||||
|
|
||||||
// Gesamtsumme - label in col 2, value in col 3
|
// Gesamtsumme - label in col 2, value in col 3
|
||||||
html.append("<tr>");
|
html.append("<tr>");
|
||||||
html.append("<td style='width:55%;padding:2px 0;'></td>");
|
html.append("<td style='width:55%;padding:2px 0;'></td>");
|
||||||
@@ -782,9 +773,7 @@ public class CustomerInvoiceService {
|
|||||||
|
|
||||||
// Get invoice data from variables
|
// Get invoice data from variables
|
||||||
String netTotal = variables.getOrDefault("invoice.net_total", "0,00 €");
|
String netTotal = variables.getOrDefault("invoice.net_total", "0,00 €");
|
||||||
String vatTotal = variables.getOrDefault("invoice.vat_total", "0,00 €");
|
|
||||||
String grossTotal = variables.getOrDefault("invoice.gross_total", "0,00 €");
|
String grossTotal = variables.getOrDefault("invoice.gross_total", "0,00 €");
|
||||||
String vatRate = variables.getOrDefault("invoice.vat_rate", "19%");
|
|
||||||
|
|
||||||
// Parse services JSON from variables
|
// Parse services JSON from variables
|
||||||
java.util.List<java.util.Map<String, String>> servicesData = new java.util.ArrayList<>();
|
java.util.List<java.util.Map<String, String>> servicesData = new java.util.ArrayList<>();
|
||||||
@@ -809,9 +798,7 @@ public class CustomerInvoiceService {
|
|||||||
// Header row
|
// Header row
|
||||||
html.append("<tr style='background-color:#f5f5f5;border-bottom:1px solid #cccccc;'>");
|
html.append("<tr style='background-color:#f5f5f5;border-bottom:1px solid #cccccc;'>");
|
||||||
html.append(
|
html.append(
|
||||||
"<th style='text-align:left;padding:4px 8px;font-weight:bold;width:55%;white-space:nowrap;'>Name</th>");
|
"<th style='text-align:left;padding:4px 8px;font-weight:bold;width:75%;white-space:nowrap;'>Name</th>");
|
||||||
html.append(
|
|
||||||
"<th style='text-align:right;padding:4px 8px;font-weight:bold;width:20%;white-space:nowrap;'>Steuersatz</th>");
|
|
||||||
html.append(
|
html.append(
|
||||||
"<th style='text-align:right;padding:4px 8px;font-weight:bold;width:25%;white-space:nowrap;'>Nettobetrag</th>");
|
"<th style='text-align:right;padding:4px 8px;font-weight:bold;width:25%;white-space:nowrap;'>Nettobetrag</th>");
|
||||||
html.append("</tr>");
|
html.append("</tr>");
|
||||||
@@ -821,23 +808,20 @@ public class CustomerInvoiceService {
|
|||||||
// Fallback: show a single row with no data
|
// Fallback: show a single row with no data
|
||||||
html.append("<tr style='border-bottom:1px solid #eeeeee;'>");
|
html.append("<tr style='border-bottom:1px solid #eeeeee;'>");
|
||||||
html.append(
|
html.append(
|
||||||
"<td colspan='3' style='text-align:center;padding:4px 8px;white-space:nowrap;'>Keine Leistungen vorhanden</td>");
|
"<td colspan='2' style='text-align:center;padding:4px 8px;white-space:nowrap;'>Keine Leistungen vorhanden</td>");
|
||||||
html.append("</tr>");
|
html.append("</tr>");
|
||||||
} else {
|
} else {
|
||||||
for (int i = 0; i < servicesData.size(); i++) {
|
for (int i = 0; i < servicesData.size(); i++) {
|
||||||
java.util.Map<String, String> service = servicesData.get(i);
|
java.util.Map<String, String> service = servicesData.get(i);
|
||||||
String name = service.getOrDefault("name", "Unbekannte Leistung");
|
String name = service.getOrDefault("name", "Unbekannte Leistung");
|
||||||
String serviceVatRate = service.getOrDefault("vatRate", vatRate);
|
|
||||||
String netAmount = service.getOrDefault("netAmount", "0,00");
|
String netAmount = service.getOrDefault("netAmount", "0,00");
|
||||||
|
|
||||||
String bgColor = (i % 2 == 1) ? "background-color:rgba(0,0,0,0.02);" : "";
|
String bgColor = (i % 2 == 1) ? "background-color:rgba(0,0,0,0.02);" : "";
|
||||||
html.append("<tr style='").append(bgColor).append("border-bottom:1px solid #eeeeee;'>");
|
html.append("<tr style='").append(bgColor).append("border-bottom:1px solid #eeeeee;'>");
|
||||||
html.append(
|
html.append(
|
||||||
"<td style='text-align:left;padding:4px 8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;'>")
|
"<td style='text-align:left;padding:4px 8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;width:75%;'>")
|
||||||
.append(escapeHtml(name)).append("</td>");
|
.append(escapeHtml(name)).append("</td>");
|
||||||
html.append("<td style='text-align:right;padding:4px 8px;white-space:nowrap;'>").append(serviceVatRate)
|
html.append("<td style='text-align:right;padding:4px 8px;white-space:nowrap;width:25%;'>").append(netAmount)
|
||||||
.append("</td>");
|
|
||||||
html.append("<td style='text-align:right;padding:4px 8px;white-space:nowrap;'>").append(netAmount)
|
|
||||||
.append(" €</td>");
|
.append(" €</td>");
|
||||||
html.append("</tr>");
|
html.append("</tr>");
|
||||||
}
|
}
|
||||||
@@ -849,9 +833,6 @@ public class CustomerInvoiceService {
|
|||||||
html.append("<div style='margin-top:8px;width:100%;'>");
|
html.append("<div style='margin-top:8px;width:100%;'>");
|
||||||
html.append("<table style='width:100%;border-collapse:collapse;font-size:inherit;table-layout:fixed;'>");
|
html.append("<table style='width:100%;border-collapse:collapse;font-size:inherit;table-layout:fixed;'>");
|
||||||
|
|
||||||
// Extract numeric value from VAT rate for display
|
|
||||||
String vatPercent = vatRate.replace(" %", "").replace("%", "");
|
|
||||||
|
|
||||||
// Nettosumme
|
// Nettosumme
|
||||||
html.append("<tr>");
|
html.append("<tr>");
|
||||||
html.append("<td style='width:55%;padding:2px 0;'></td>");
|
html.append("<td style='width:55%;padding:2px 0;'></td>");
|
||||||
@@ -860,15 +841,6 @@ public class CustomerInvoiceService {
|
|||||||
.append(netTotal).append("</td>");
|
.append(netTotal).append("</td>");
|
||||||
html.append("</tr>");
|
html.append("</tr>");
|
||||||
|
|
||||||
// Umsatzsteuer
|
|
||||||
html.append("<tr>");
|
|
||||||
html.append("<td style='width:55%;padding:2px 0;'></td>");
|
|
||||||
html.append("<td style='width:20%;text-align:left;padding:2px 8px;white-space:nowrap;'>zzgl. ")
|
|
||||||
.append(vatPercent).append("% USt:</td>");
|
|
||||||
html.append("<td style='width:25%;text-align:right;padding:2px 8px;white-space:nowrap;font-weight:bold;'>")
|
|
||||||
.append(vatTotal).append("</td>");
|
|
||||||
html.append("</tr>");
|
|
||||||
|
|
||||||
// Gesamtsumme
|
// Gesamtsumme
|
||||||
html.append("<tr>");
|
html.append("<tr>");
|
||||||
html.append("<td style='width:55%;padding:2px 0;'></td>");
|
html.append("<td style='width:55%;padding:2px 0;'></td>");
|
||||||
|
|||||||
@@ -531,7 +531,9 @@ addjob.services.vat=Mehrwertsteuer
|
|||||||
addjob.services.route.missing=Route fehlt
|
addjob.services.route.missing=Route fehlt
|
||||||
addjob.services.dialog.title=Leistung auswählen
|
addjob.services.dialog.title=Leistung auswählen
|
||||||
addjob.services.dialog.placeholder=Leistung wählen
|
addjob.services.dialog.placeholder=Leistung wählen
|
||||||
|
addjob.services.dialog.station.placeholder=Lieferstation wählen
|
||||||
addjob.services.dialog.add=Hinzufügen
|
addjob.services.dialog.add=Hinzufügen
|
||||||
|
addjob.services.deliverystation=Lieferstation
|
||||||
addjob.summary.title=Zusammenfassung
|
addjob.summary.title=Zusammenfassung
|
||||||
addjob.summary.net=Netto
|
addjob.summary.net=Netto
|
||||||
addjob.summary.vat=Mehrwertsteuer
|
addjob.summary.vat=Mehrwertsteuer
|
||||||
@@ -937,4 +939,4 @@ adminpricetable.field.applicense=App-Nutzungslizenz
|
|||||||
adminpricetable.field.revenue=Umsatzbeteiligung
|
adminpricetable.field.revenue=Umsatzbeteiligung
|
||||||
adminpricetable.notification.saved=Preistabelle wurde gespeichert
|
adminpricetable.notification.saved=Preistabelle wurde gespeichert
|
||||||
adminpricetable.notification.save.error=Fehler beim Speichern: {0}
|
adminpricetable.notification.save.error=Fehler beim Speichern: {0}
|
||||||
adminpricetable.notification.load.error=Fehler beim Laden: {0}
|
adminpricetable.notification.load.error=Fehler beim Laden: {0}
|
||||||
|
|||||||
@@ -531,7 +531,9 @@ addjob.services.vat=VAT
|
|||||||
addjob.services.route.missing=Route missing
|
addjob.services.route.missing=Route missing
|
||||||
addjob.services.dialog.title=Select Service
|
addjob.services.dialog.title=Select Service
|
||||||
addjob.services.dialog.placeholder=Select service
|
addjob.services.dialog.placeholder=Select service
|
||||||
|
addjob.services.dialog.station.placeholder=Select delivery station
|
||||||
addjob.services.dialog.add=Add
|
addjob.services.dialog.add=Add
|
||||||
|
addjob.services.deliverystation=Delivery Station
|
||||||
addjob.summary.title=Summary
|
addjob.summary.title=Summary
|
||||||
addjob.summary.net=Net
|
addjob.summary.net=Net
|
||||||
addjob.summary.vat=VAT
|
addjob.summary.vat=VAT
|
||||||
@@ -936,4 +938,4 @@ adminpricetable.field.applicense=App Usage License
|
|||||||
adminpricetable.field.revenue=Revenue Participation
|
adminpricetable.field.revenue=Revenue Participation
|
||||||
adminpricetable.notification.saved=Price table has been saved
|
adminpricetable.notification.saved=Price table has been saved
|
||||||
adminpricetable.notification.save.error=Error saving: {0}
|
adminpricetable.notification.save.error=Error saving: {0}
|
||||||
adminpricetable.notification.load.error=Error loading: {0}
|
adminpricetable.notification.load.error=Error loading: {0}
|
||||||
|
|||||||
@@ -422,10 +422,6 @@
|
|||||||
<td class="label-col">Nettobetrag:</td>
|
<td class="label-col">Nettobetrag:</td>
|
||||||
<td class="amount-col">${invoiceData.netAmount}</td>
|
<td class="amount-col">${invoiceData.netAmount}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td class="label-col">zzgl. ${invoiceData.vatRate} USt.:</td>
|
|
||||||
<td class="amount-col">${invoiceData.vatAmount}</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="total-row">
|
<tr class="total-row">
|
||||||
<td class="label-col">Rechnungsbetrag:</td>
|
<td class="label-col">Rechnungsbetrag:</td>
|
||||||
<td class="amount-col">${invoiceData.totalAmount}</td>
|
<td class="amount-col">${invoiceData.totalAmount}</td>
|
||||||
@@ -465,4 +461,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user