feat: adapt station pricing and route handling
This commit is contained in:
@@ -148,6 +148,10 @@ public class Job {
|
||||
@Field("service_ids")
|
||||
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)
|
||||
@Field("route_distance_km")
|
||||
private Double routeDistanceKm;
|
||||
@@ -227,4 +231,4 @@ public class Job {
|
||||
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")
|
||||
public class Service {
|
||||
public static final BigDecimal FIXED_VAT_RATE = new BigDecimal("0.19");
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
@@ -16,7 +17,6 @@ public class Service {
|
||||
private BigDecimal price; // For FLAT_RATE services
|
||||
private BigDecimal pricePerKilometer; // For DISTANCE services - price per kilometer
|
||||
private BigDecimal pricePer15Minutes; // For TIME services - price per 15 minutes
|
||||
private BigDecimal vatRate;
|
||||
private boolean mandatory;
|
||||
|
||||
public enum CalculationBasis {
|
||||
@@ -36,7 +36,6 @@ public class Service {
|
||||
this.userId = userId;
|
||||
this.name = name;
|
||||
this.calculationBasis = calculationBasis;
|
||||
this.vatRate = vatRate;
|
||||
this.mandatory = mandatory;
|
||||
|
||||
// Set the appropriate price field based on calculation basis
|
||||
@@ -86,11 +85,10 @@ public class Service {
|
||||
}
|
||||
|
||||
public BigDecimal getVatRate() {
|
||||
return vatRate;
|
||||
return FIXED_VAT_RATE;
|
||||
}
|
||||
|
||||
public void setVatRate(BigDecimal vatRate) {
|
||||
this.vatRate = vatRate;
|
||||
}
|
||||
|
||||
public BigDecimal getPrice() {
|
||||
|
||||
@@ -28,7 +28,6 @@ import de.assecutor.votianlt.pages.service.AddressValidationService;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
@@ -199,7 +198,6 @@ public class DeliveryStationDialog extends Dialog {
|
||||
private Span tasksTabError;
|
||||
|
||||
private final DeliveryStationTile.TranslationHelper translationHelper;
|
||||
private final AddressValidationService addressValidationService;
|
||||
private final java.util.Map<String, Customer> companyAddressOptions = new java.util.LinkedHashMap<>();
|
||||
|
||||
public DeliveryStationDialog(String dialogTitle, List<Customer> customers,
|
||||
@@ -208,8 +206,6 @@ public class DeliveryStationDialog extends Dialog {
|
||||
AddressValidationService addressValidationService) {
|
||||
|
||||
this.translationHelper = translationHelper;
|
||||
this.addressValidationService = addressValidationService;
|
||||
|
||||
setHeaderTitle(dialogTitle);
|
||||
setCloseOnOutsideClick(false);
|
||||
setWidth("960px");
|
||||
@@ -419,6 +415,7 @@ public class DeliveryStationDialog extends Dialog {
|
||||
if (data == null)
|
||||
return;
|
||||
String companyOption = findCompanyOptionLabel(data);
|
||||
boolean customerSelectedFromOptions = companyOption != null;
|
||||
if (companyOption != null) {
|
||||
company.setValue(companyOption);
|
||||
} else if (data.getCompany() != null) {
|
||||
@@ -442,7 +439,8 @@ public class DeliveryStationDialog extends Dialog {
|
||||
zip.setValue(data.getZip());
|
||||
if (data.getCity() != null)
|
||||
city.setValue(data.getCity());
|
||||
saveAddress.setValue(data.isSaveAddress());
|
||||
saveAddress.setValue(customerSelectedFromOptions ? false : data.isSaveAddress());
|
||||
updateSaveAddressState(customerSelectedFromOptions);
|
||||
|
||||
// Load tasks into dialog state
|
||||
if (data.getTasks() != null && !data.getTasks().isEmpty()) {
|
||||
@@ -562,6 +560,7 @@ public class DeliveryStationDialog extends Dialog {
|
||||
|
||||
companyField.addValueChangeListener(event -> {
|
||||
Customer customer = companyAddressOptions.get(event.getValue());
|
||||
updateSaveAddressState(customer != null);
|
||||
if (customer == null) {
|
||||
return;
|
||||
}
|
||||
@@ -589,7 +588,20 @@ public class DeliveryStationDialog extends Dialog {
|
||||
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) {
|
||||
|
||||
@@ -250,14 +250,10 @@ public class PickupStationDialog extends Dialog {
|
||||
private Span cargoTabError;
|
||||
|
||||
private final DeliveryStationTile.TranslationHelper translationHelper;
|
||||
private final AddressValidationService addressValidationService;
|
||||
|
||||
public PickupStationDialog(String dialogTitle, List<Customer> customers,
|
||||
DeliveryStationTile.TranslationHelper translationHelper, SaveListener saveListener,
|
||||
List<AppUser> availableAppUsers, AddressValidationService addressValidationService) {
|
||||
|
||||
this.addressValidationService = addressValidationService;
|
||||
|
||||
this.translationHelper = translationHelper;
|
||||
|
||||
setHeaderTitle(dialogTitle);
|
||||
|
||||
@@ -32,6 +32,7 @@ import com.vaadin.flow.theme.lumo.LumoUtility;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
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.BaseTask;
|
||||
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_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
|
||||
public String getPageTitle() {
|
||||
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<DeliveryStation> deliveryStationsState = 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 addStationButton;
|
||||
private Div addStationButtonSlot;
|
||||
private Div pickupStationSlot;
|
||||
private StationTile pickupTile;
|
||||
private static final int MAX_DELIVERY_STATIONS = 7;
|
||||
|
||||
@@ -125,10 +156,9 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
private ComboBox<AppUser> appUser;
|
||||
|
||||
// Services for the job
|
||||
private Grid<Service> servicesGrid;
|
||||
private final List<Service> selectedServices = new ArrayList<>();
|
||||
private Grid<SelectedServiceEntry> servicesGrid;
|
||||
private final List<SelectedServiceEntry> selectedServices = new ArrayList<>();
|
||||
private Span netTotalLabel;
|
||||
private Span vatTotalLabel;
|
||||
private Span grossTotalLabel;
|
||||
|
||||
// Route distance display
|
||||
@@ -173,6 +203,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
private RouteCalculationResult routeCalculationResult;
|
||||
private boolean pickupAddressValidatedByGoogle;
|
||||
private final List<Boolean> deliveryStationsValidatedByGoogle = new ArrayList<>();
|
||||
private final Map<Integer, RouteCalculationResult> pickupToDeliveryRouteResults = new HashMap<>();
|
||||
|
||||
public AddJobView(AddJobService addJobService, AddCustomerService addCustomerService,
|
||||
CustomerService customerService, AppUserService appUserService, TaskTemplateService taskTemplateService,
|
||||
@@ -342,10 +373,12 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
// Pickup tile (always present)
|
||||
pickupTile = new StationTile(StationTile.StationType.PICKUP, 0, getTranslation("addjob.section.pickup"), false);
|
||||
pickupTile.setClickListener(tile -> openPickupDialog());
|
||||
stationsGridContainer.add(pickupTile);
|
||||
pickupStationSlot = createStationSlot(pickupTile, null);
|
||||
stationsGridContainer.add(pickupStationSlot);
|
||||
|
||||
// "+" add station button tile (must be created before addDeliveryStationTile)
|
||||
addStationButton = createAddStationButton();
|
||||
addStationButtonSlot = createStationSlot(addStationButton, null);
|
||||
|
||||
// Add first delivery station tile (this will also add the "+" button)
|
||||
addDeliveryStationTile();
|
||||
@@ -454,6 +487,12 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
manualDurationInput.setMin(0);
|
||||
manualDurationInput.setStep(1);
|
||||
manualDurationInput.setClearButtonVisible(true);
|
||||
manualDurationInput.addValueChangeListener(e -> {
|
||||
updatePriceSummary();
|
||||
if (servicesGrid != null) {
|
||||
servicesGrid.getDataProvider().refreshAll();
|
||||
}
|
||||
});
|
||||
|
||||
manualInputRow.add(manualDistanceInput, manualDurationInput);
|
||||
|
||||
@@ -476,8 +515,12 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
servicesGrid.setHeight("250px");
|
||||
servicesGrid.setItems(selectedServices);
|
||||
|
||||
servicesGrid.addColumn(Service::getName).setHeader(getTranslation("common.service")).setSortable(true);
|
||||
servicesGrid.addColumn(service -> {
|
||||
servicesGrid.addColumn(entry -> entry.getService().getName()).setHeader(getTranslation("common.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) {
|
||||
return switch (service.getCalculationBasis()) {
|
||||
case DISTANCE -> getTranslation("addjob.services.basis.distance");
|
||||
@@ -487,29 +530,28 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
}
|
||||
return "";
|
||||
}).setHeader(getTranslation("addjob.services.calculation")).setSortable(true);
|
||||
servicesGrid.addColumn(service -> {
|
||||
// Get route distance for distance-based calculations (berechnet oder manuell)
|
||||
Double routeDistance = getEffectiveRouteDistance();
|
||||
BigDecimal price = calculateServicePrice(service, routeDistance);
|
||||
servicesGrid.addColumn(entry -> {
|
||||
Service service = entry.getService();
|
||||
Double routeDistance = getEffectiveRouteDistance(entry);
|
||||
Integer durationSeconds = getEffectiveRouteDuration(entry);
|
||||
BigDecimal price = calculateServicePrice(service, routeDistance, durationSeconds);
|
||||
if (price.compareTo(BigDecimal.ZERO) > 0) {
|
||||
return price.setScale(2, RoundingMode.HALF_UP) + " €";
|
||||
}
|
||||
// Show price info if no route calculated yet
|
||||
if (service.getCalculationBasis() == Service.CalculationBasis.DISTANCE && routeDistance == null) {
|
||||
return service.getPricePerKilometer().setScale(2, RoundingMode.HALF_UP) + " €/km ("
|
||||
+ 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
|
||||
? service.getEffectivePrice().setScale(2, RoundingMode.HALF_UP) + " €"
|
||||
: "";
|
||||
}).setHeader(getTranslation("common.price")).setSortable(false);
|
||||
servicesGrid.addColumn(service -> {
|
||||
if (service.getVatRate() != null) {
|
||||
return service.getVatRate().multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP) + " %";
|
||||
}
|
||||
return "";
|
||||
}).setHeader(getTranslation("addjob.services.vat")).setSortable(true);
|
||||
servicesGrid.addComponentColumn(service -> {
|
||||
servicesGrid.addComponentColumn(entry -> {
|
||||
Service service = entry.getService();
|
||||
// Verbindliche Leistungen können nicht gelöscht werden
|
||||
if (service.isMandatory()) {
|
||||
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,
|
||||
ButtonVariant.LUMO_SMALL);
|
||||
removeButton.addClickListener(e -> {
|
||||
selectedServices.remove(service);
|
||||
selectedServices.remove(entry);
|
||||
servicesGrid.getDataProvider().refreshAll();
|
||||
updatePriceSummary();
|
||||
triggerValidation();
|
||||
@@ -561,16 +603,6 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
netRow.add(netLabelSpan, netTotalLabel);
|
||||
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
|
||||
Div grossRow = new Div();
|
||||
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;
|
||||
}
|
||||
|
||||
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() {
|
||||
int stationNumber = deliveryStationTilesList.size() + 1;
|
||||
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.setDeleteListener(this::removeDeliveryStationTile);
|
||||
|
||||
Span distanceChip = createDeliveryDistanceChip();
|
||||
Div stationSlot = createStationSlot(tile, distanceChip);
|
||||
|
||||
deliveryStationTilesList.add(tile);
|
||||
deliveryStationSlotList.add(stationSlot);
|
||||
deliveryStationDistanceChips.add(distanceChip);
|
||||
|
||||
// Rebuild grid: remove plus button, add tile, re-add plus button
|
||||
stationsGridContainer.remove(addStationButton);
|
||||
stationsGridContainer.add(tile);
|
||||
stationsGridContainer.remove(addStationButtonSlot);
|
||||
stationsGridContainer.add(stationSlot);
|
||||
|
||||
// Hide "+" button if max reached
|
||||
if (deliveryStationTilesList.size() < MAX_DELIVERY_STATIONS) {
|
||||
stationsGridContainer.add(addStationButton);
|
||||
stationsGridContainer.add(addStationButtonSlot);
|
||||
}
|
||||
|
||||
resetRouteInformation();
|
||||
@@ -683,6 +755,9 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
deliveryStationsSaveAddress.remove(removeIdx);
|
||||
deliveryStationsValidatedByGoogle.remove(removeIdx);
|
||||
deliveryStationTasksState.remove(removeIdx);
|
||||
Div removedSlot = deliveryStationSlotList.remove(removeIdx);
|
||||
deliveryStationDistanceChips.remove(removeIdx);
|
||||
pickupToDeliveryRouteResults.remove(removeIdx);
|
||||
// Re-index tasks state for remaining stations
|
||||
Map<Integer, List<BaseTask>> reindexed = new HashMap<>();
|
||||
for (Map.Entry<Integer, List<BaseTask>> entry : deliveryStationTasksState.entrySet()) {
|
||||
@@ -692,7 +767,28 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
}
|
||||
deliveryStationTasksState.clear();
|
||||
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
|
||||
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
|
||||
if (deliveryStationTilesList.size() < MAX_DELIVERY_STATIONS && addStationButton.getParent().isEmpty()) {
|
||||
stationsGridContainer.add(addStationButton);
|
||||
if (deliveryStationTilesList.size() < MAX_DELIVERY_STATIONS && addStationButtonSlot.getParent().isEmpty()) {
|
||||
stationsGridContainer.add(addStationButtonSlot);
|
||||
}
|
||||
|
||||
resetRouteInformation();
|
||||
resetStationsAppliedState();
|
||||
if (servicesGrid != null) {
|
||||
servicesGrid.getDataProvider().refreshAll();
|
||||
}
|
||||
updatePriceSummary();
|
||||
triggerValidation();
|
||||
updateTabLabels();
|
||||
});
|
||||
@@ -1075,7 +1175,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
private void openAddServiceDialog() {
|
||||
Dialog dialog = new Dialog();
|
||||
dialog.setHeaderTitle(getTranslation("addjob.services.dialog.title"));
|
||||
dialog.setWidth("500px");
|
||||
dialog.setWidth("560px");
|
||||
|
||||
VerticalLayout dialogContent = new VerticalLayout();
|
||||
dialogContent.setPadding(true);
|
||||
@@ -1099,7 +1199,18 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
serviceCombo.setPlaceholder(getTranslation("addjob.services.dialog.placeholder"));
|
||||
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();
|
||||
buttonLayout.setWidthFull();
|
||||
@@ -1110,8 +1221,8 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
cancelButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||
|
||||
Button addButton = new Button(getTranslation("addjob.services.dialog.add"), e -> {
|
||||
if (serviceCombo.getValue() != null) {
|
||||
selectedServices.add(serviceCombo.getValue());
|
||||
if (serviceCombo.getValue() != null && deliveryStationCombo.getValue() != null) {
|
||||
selectedServices.add(new SelectedServiceEntry(serviceCombo.getValue(), deliveryStationCombo.getValue()));
|
||||
servicesGrid.getDataProvider().refreshAll();
|
||||
updatePriceSummary();
|
||||
triggerValidation();
|
||||
@@ -1130,33 +1241,61 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
|
||||
private void updatePriceSummary() {
|
||||
BigDecimal netTotal = BigDecimal.ZERO;
|
||||
BigDecimal vatTotal = BigDecimal.ZERO;
|
||||
BigDecimal grossTotal = BigDecimal.ZERO;
|
||||
|
||||
// Get route distance for distance-based calculations (berechnet oder manuell)
|
||||
Double routeDistance = getEffectiveRouteDistance();
|
||||
|
||||
for (Service service : selectedServices) {
|
||||
BigDecimal price = calculateServicePrice(service, routeDistance);
|
||||
BigDecimal vatRate = service.getVatRate() != null ? service.getVatRate() : BigDecimal.ZERO;
|
||||
for (SelectedServiceEntry entry : selectedServices) {
|
||||
Service service = entry.getService();
|
||||
BigDecimal price = calculateServicePrice(service, getEffectiveRouteDistance(entry),
|
||||
getEffectiveRouteDuration(entry));
|
||||
BigDecimal vatRate = Service.FIXED_VAT_RATE;
|
||||
|
||||
netTotal = netTotal.add(price);
|
||||
BigDecimal vatAmount = price.multiply(vatRate);
|
||||
vatTotal = vatTotal.add(vatAmount);
|
||||
grossTotal = grossTotal.add(price.add(vatAmount));
|
||||
}
|
||||
|
||||
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(".", ",") + " €");
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the actual price for a service based on its calculation basis and
|
||||
* route distance (for distance-based services).
|
||||
*/
|
||||
private BigDecimal calculateServicePrice(Service service, Double routeDistance) {
|
||||
return calculateServicePrice(service, routeDistance, null);
|
||||
private List<Integer> getAvailableDeliveryStationOrders() {
|
||||
List<Integer> stationOrders = new ArrayList<>();
|
||||
for (int i = 0; i < deliveryStationsState.size(); i++) {
|
||||
stationOrders.add(i);
|
||||
}
|
||||
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) {
|
||||
@@ -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()
|
||||
// removed - replaced by StationTile + StationDialog
|
||||
|
||||
@@ -1577,15 +1754,15 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
job.syncFlatDeliveryFieldsFromStations();
|
||||
|
||||
// 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
|
||||
if (binder.writeBeanIfValid(job)) {
|
||||
// Preis nach dem Binder-Call berechnen (damit er nicht überschrieben wird)
|
||||
Double routeDistance = getEffectiveRouteDistance();
|
||||
Integer durationSeconds = getEffectiveRouteDuration();
|
||||
BigDecimal netTotal = selectedServices.stream()
|
||||
.map(s -> calculateServicePrice(s, routeDistance, durationSeconds))
|
||||
.map(entry -> calculateServicePrice(entry.getService(), getEffectiveRouteDistance(entry),
|
||||
getEffectiveRouteDuration(entry)))
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
job.setPrice(netTotal);
|
||||
|
||||
@@ -1706,7 +1883,8 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
List<Service> mandatoryServices = userServices.stream().filter(Service::isMandatory).toList();
|
||||
|
||||
if (!mandatoryServices.isEmpty()) {
|
||||
selectedServices.addAll(mandatoryServices);
|
||||
mandatoryServices.stream().map(service -> new SelectedServiceEntry(service, 0))
|
||||
.forEach(selectedServices::add);
|
||||
if (servicesGrid != null) {
|
||||
servicesGrid.getDataProvider().refreshAll();
|
||||
}
|
||||
@@ -1908,7 +2086,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
UI ui = UI.getCurrent();
|
||||
loadingDialog.open();
|
||||
|
||||
CompletableFuture.supplyAsync(this::calculateRouteAcrossAllStations).whenComplete((routeResult, throwable) -> {
|
||||
CompletableFuture.supplyAsync(this::calculateRouteBundle).whenComplete((routeBundle, throwable) -> {
|
||||
if (ui == null) {
|
||||
return;
|
||||
}
|
||||
@@ -1921,8 +2099,9 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
return;
|
||||
}
|
||||
|
||||
RouteCalculationResult routeResult = routeBundle != null ? routeBundle.totalRoute() : null;
|
||||
if (routeResult != null && routeResult.isValid()) {
|
||||
applyCalculatedRoute(routeResult);
|
||||
applyCalculatedRoutes(routeBundle);
|
||||
showRouteSummaryDialog(routeResult);
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
|
||||
private RouteCalculationResult calculateRouteAcrossAllStations() {
|
||||
private RouteCalculationBundle calculateRouteBundle() {
|
||||
if (!pickupAddressValidatedByGoogle) {
|
||||
return createInvalidRouteResult("Die Abholstation ist nicht validiert.");
|
||||
return createInvalidRouteBundle("Die Abholstation ist nicht validiert.");
|
||||
}
|
||||
|
||||
List<AddressValidationResult> stationResults = new ArrayList<>();
|
||||
AddressValidationResult pickupValidation = getOrValidatePickupAddressResult();
|
||||
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++) {
|
||||
DeliveryStation station = deliveryStationsState.get(i);
|
||||
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))) {
|
||||
return createInvalidRouteResult("Nicht alle Lieferstationen sind validiert.");
|
||||
if (i >= deliveryStationsValidatedByGoogle.size()
|
||||
|| !Boolean.TRUE.equals(deliveryStationsValidatedByGoogle.get(i))) {
|
||||
return createInvalidRouteBundle("Nicht alle Lieferstationen sind validiert.");
|
||||
}
|
||||
|
||||
AddressValidationResult deliveryValidation = getOrValidateDeliveryAddressResult(i);
|
||||
if (deliveryValidation == null || !deliveryValidation.isValid()) {
|
||||
return createInvalidRouteResult(
|
||||
return createInvalidRouteBundle(
|
||||
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() {
|
||||
@@ -2028,10 +2257,8 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
addressValidationResults.put(resultKey, validationResult);
|
||||
}
|
||||
|
||||
private RouteCalculationResult createInvalidRouteResult(String message) {
|
||||
RouteCalculationResult routeResult = new RouteCalculationResult();
|
||||
routeResult.setRouteMessage(message);
|
||||
return routeResult;
|
||||
private RouteCalculationBundle createInvalidRouteBundle(String message) {
|
||||
return new RouteCalculationBundle(createInvalidRouteResult(message), Map.of());
|
||||
}
|
||||
|
||||
private Dialog createRouteLoadingDialog() {
|
||||
@@ -2055,8 +2282,14 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
return dialog;
|
||||
}
|
||||
|
||||
private void applyCalculatedRoute(RouteCalculationResult routeResult) {
|
||||
private void applyCalculatedRoutes(RouteCalculationBundle routeBundle) {
|
||||
RouteCalculationResult routeResult = routeBundle.totalRoute();
|
||||
routeCalculationResult = routeResult;
|
||||
pickupToDeliveryRouteResults.clear();
|
||||
if (routeBundle.deliveryRoutes() != null) {
|
||||
pickupToDeliveryRouteResults.putAll(routeBundle.deliveryRoutes());
|
||||
}
|
||||
renderDeliveryStationDistanceChips();
|
||||
|
||||
routeDistanceLabel.setText(routeResult.getFormattedDistance());
|
||||
routeDurationLabel.setText(routeResult.getFormattedDurationLong());
|
||||
@@ -2103,6 +2336,25 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
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
|
||||
* Streckeninformationen zurückzusetzen.
|
||||
@@ -2179,6 +2431,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
private void resetRouteInformation() {
|
||||
// Routenberechnung zurücksetzen
|
||||
routeCalculationResult = null;
|
||||
pickupToDeliveryRouteResults.clear();
|
||||
|
||||
// Validierungsergebnisse zurücksetzen
|
||||
addressValidationResults.clear();
|
||||
@@ -2196,6 +2449,10 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
if (manualRouteInputBox != null) {
|
||||
manualRouteInputBox.setVisible(true);
|
||||
}
|
||||
for (Span chip : deliveryStationDistanceChips) {
|
||||
chip.setText("");
|
||||
chip.setVisible(false);
|
||||
}
|
||||
|
||||
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 de.assecutor.votianlt.model.Customer;
|
||||
import de.assecutor.votianlt.model.Job;
|
||||
import de.assecutor.votianlt.model.JobServiceSelection;
|
||||
import de.assecutor.votianlt.model.Service;
|
||||
import de.assecutor.votianlt.model.User;
|
||||
import de.assecutor.votianlt.model.InvoiceTemplate;
|
||||
@@ -73,6 +74,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
||||
*/
|
||||
public static class ServiceRow {
|
||||
private Service service;
|
||||
private JobServiceSelection selection;
|
||||
|
||||
public ServiceRow() {
|
||||
this.service = null;
|
||||
@@ -82,6 +84,11 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
public ServiceRow(Service service, JobServiceSelection selection) {
|
||||
this.service = service;
|
||||
this.selection = selection;
|
||||
}
|
||||
|
||||
public Service getService() {
|
||||
return service;
|
||||
}
|
||||
@@ -90,6 +97,14 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
public JobServiceSelection getSelection() {
|
||||
return selection;
|
||||
}
|
||||
|
||||
public void setSelection(JobServiceSelection selection) {
|
||||
this.selection = selection;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
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.
|
||||
*/
|
||||
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()) {
|
||||
gridRows.clear();
|
||||
for (String serviceId : currentJob.getServiceIds()) {
|
||||
@@ -267,6 +295,9 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
||||
return "";
|
||||
}).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)
|
||||
servicesGrid.addColumn(row -> {
|
||||
if (row.getService() != null && row.getService().getCalculationBasis() != null) {
|
||||
@@ -282,7 +313,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
||||
// Price column (read-only)
|
||||
servicesGrid.addColumn(row -> {
|
||||
if (row.getService() != null) {
|
||||
BigDecimal price = calculateServicePrice(row.getService());
|
||||
BigDecimal price = calculateServicePrice(row);
|
||||
if (price != null) {
|
||||
return price.setScale(2, RoundingMode.HALF_UP) + " €";
|
||||
}
|
||||
@@ -296,8 +327,8 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
||||
return servicesSection;
|
||||
}
|
||||
|
||||
private List<Service> getSelectedServices() {
|
||||
return gridRows.stream().filter(row -> row.getService() != null).map(ServiceRow::getService).toList();
|
||||
private List<ServiceRow> getSelectedServices() {
|
||||
return gridRows.stream().filter(row -> row.getService() != null).toList();
|
||||
}
|
||||
|
||||
private Div createSummarySection() {
|
||||
@@ -311,7 +342,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
||||
|
||||
// Calculate totals
|
||||
BigDecimal netAmount = calculateNetAmount();
|
||||
BigDecimal vatRate = calculateAverageVatRate();
|
||||
BigDecimal vatRate = Service.FIXED_VAT_RATE;
|
||||
BigDecimal vatAmount = netAmount.multiply(vatRate);
|
||||
BigDecimal totalAmount = netAmount.add(vatAmount);
|
||||
|
||||
@@ -320,10 +351,6 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
||||
|
||||
priceTable.add(createPriceRow(getTranslation("createinvoice.summary.net") + ":",
|
||||
netAmount.setScale(2, RoundingMode.HALF_UP) + " €", false));
|
||||
priceTable.add(createPriceRow(
|
||||
getTranslation("createinvoice.summary.vat",
|
||||
vatRate.multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP).toString()) + ":",
|
||||
vatAmount.setScale(2, RoundingMode.HALF_UP) + " €", false));
|
||||
priceTable.add(createPriceRow(getTranslation("createinvoice.summary.total") + ":",
|
||||
totalAmount.setScale(2, RoundingMode.HALF_UP) + " €", true));
|
||||
|
||||
@@ -349,7 +376,17 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
@@ -357,32 +394,55 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
||||
if (service.getCalculationBasis() == Service.CalculationBasis.FLAT_RATE && service.getPrice() != null) {
|
||||
return service.getPrice();
|
||||
} else if (service.getCalculationBasis() == Service.CalculationBasis.DISTANCE
|
||||
&& service.getPricePerKilometer() != null && currentJob.getRouteDistanceKm() != null) {
|
||||
BigDecimal kilometers = BigDecimal.valueOf(currentJob.getRouteDistanceKm());
|
||||
&& service.getPricePerKilometer() != null && getRouteDistanceKm(selection) != null) {
|
||||
BigDecimal kilometers = BigDecimal.valueOf(getRouteDistanceKm(selection));
|
||||
return service.getPricePerKilometer().multiply(kilometers);
|
||||
} else if (service.getCalculationBasis() == Service.CalculationBasis.TIME
|
||||
&& service.getPricePer15Minutes() != null && currentJob.getTimeIn15MinUnits() != null) {
|
||||
BigDecimal timeUnits = new BigDecimal(currentJob.getTimeIn15MinUnits());
|
||||
&& service.getPricePer15Minutes() != null && getTimeIn15MinUnits(selection) != null) {
|
||||
BigDecimal timeUnits = new BigDecimal(getTimeIn15MinUnits(selection));
|
||||
return service.getPricePer15Minutes().multiply(timeUnits);
|
||||
}
|
||||
|
||||
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() {
|
||||
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) {
|
||||
total = total.add(service.getPrice());
|
||||
} else if (service.getCalculationBasis() == Service.CalculationBasis.DISTANCE
|
||||
&& service.getPricePerKilometer() != null && currentJob.getRouteDistanceKm() != null) {
|
||||
BigDecimal kilometers = BigDecimal.valueOf(currentJob.getRouteDistanceKm());
|
||||
&& service.getPricePerKilometer() != null && getRouteDistanceKm(row.getSelection()) != null) {
|
||||
BigDecimal kilometers = BigDecimal.valueOf(getRouteDistanceKm(row.getSelection()));
|
||||
BigDecimal serviceTotal = service.getPricePerKilometer().multiply(kilometers);
|
||||
total = total.add(serviceTotal);
|
||||
} else if (service.getCalculationBasis() == Service.CalculationBasis.TIME
|
||||
&& service.getPricePer15Minutes() != null && currentJob.getTimeIn15MinUnits() != null) {
|
||||
BigDecimal timeUnits = new BigDecimal(currentJob.getTimeIn15MinUnits());
|
||||
&& service.getPricePer15Minutes() != null && getTimeIn15MinUnits(row.getSelection()) != null) {
|
||||
BigDecimal timeUnits = new BigDecimal(getTimeIn15MinUnits(row.getSelection()));
|
||||
BigDecimal serviceTotal = service.getPricePer15Minutes().multiply(timeUnits);
|
||||
total = total.add(serviceTotal);
|
||||
}
|
||||
@@ -391,29 +451,6 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
||||
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) {
|
||||
if (customerSelection == null || customerSelection.isBlank()) {
|
||||
return "";
|
||||
@@ -478,7 +515,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
||||
String invoiceNumber = userInvoiceDataService.generateNextInvoiceNumber(user.getId());
|
||||
|
||||
BigDecimal netAmount = calculateNetAmount();
|
||||
BigDecimal vatRate = calculateAverageVatRate();
|
||||
BigDecimal vatRate = Service.FIXED_VAT_RATE;
|
||||
BigDecimal vatAmount = netAmount.multiply(vatRate);
|
||||
BigDecimal totalAmount = netAmount.add(vatAmount);
|
||||
|
||||
@@ -517,7 +554,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
||||
throws Exception {
|
||||
// Calculate totals
|
||||
BigDecimal netAmount = calculateNetAmount();
|
||||
BigDecimal vatRate = calculateAverageVatRate();
|
||||
BigDecimal vatRate = Service.FIXED_VAT_RATE;
|
||||
BigDecimal vatAmount = netAmount.multiply(vatRate);
|
||||
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
|
||||
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<>();
|
||||
serviceData.put("name", service.getName());
|
||||
|
||||
// Calculate price based on calculation basis
|
||||
BigDecimal price = calculateServicePrice(service);
|
||||
BigDecimal price = calculateServicePrice(row);
|
||||
if (price != null) {
|
||||
serviceData.put("netAmount", price.setScale(2, RoundingMode.HALF_UP).toString().replace(".", ","));
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -711,4 +741,4 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
||||
public String getPageTitle() {
|
||||
return getTranslation("page.title.invoice.create");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,6 +318,7 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
||||
bankNameField = new TextField();
|
||||
ibanField = new TextField();
|
||||
taxRateField = new TextField();
|
||||
taxRateField.setValue("19");
|
||||
introTextArea = new TextArea();
|
||||
termsTextArea = new TextArea();
|
||||
pdfFrame = new IFrame();
|
||||
@@ -646,8 +647,6 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
||||
bankNameField.setEnabled(enabled);
|
||||
if (ibanField != null)
|
||||
ibanField.setEnabled(enabled);
|
||||
if (taxRateField != null)
|
||||
taxRateField.setEnabled(enabled);
|
||||
if (introTextArea != null)
|
||||
introTextArea.setEnabled(enabled);
|
||||
if (termsTextArea != null)
|
||||
@@ -710,7 +709,7 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
||||
|
||||
// Create sample invoice items for preview
|
||||
List<CustomerInvoiceItem> items = new ArrayList<>();
|
||||
BigDecimal vatRate = parseVatRate(safe(taxRateField));
|
||||
BigDecimal vatRate = Service.FIXED_VAT_RATE;
|
||||
|
||||
CustomerInvoiceItem item1 = new CustomerInvoiceItem(new BigDecimal("1"), "Stk.",
|
||||
"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
|
||||
private String safe(TextField f) {
|
||||
return f != null && f.getValue() != null ? f.getValue() : "";
|
||||
@@ -789,7 +770,7 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
||||
taxNumberField.setValue(safe(currentInvoiceData.getTaxNumber()));
|
||||
bankNameField.setValue(safe(currentInvoiceData.getBankName()));
|
||||
ibanField.setValue(safe(currentInvoiceData.getIban()));
|
||||
taxRateField.setValue(safe(currentInvoiceData.getTaxRate()));
|
||||
taxRateField.setValue("19");
|
||||
introTextArea.setValue(safe(currentInvoiceData.getIntroText()));
|
||||
termsTextArea.setValue(safe(currentInvoiceData.getPaymentTerms()));
|
||||
|
||||
@@ -804,7 +785,7 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
||||
private void saveInvoiceData() {
|
||||
currentInvoiceData = userInvoiceDataService.createOrUpdate(currentUser.getId(), billingEnabled.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) {
|
||||
@@ -979,8 +960,6 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
||||
VaadinIcon.LIST, "services.list", "Artikel 1: 100,00 €\nArtikel 2: 50,00 €");
|
||||
Div servicesNetBlock = createServicesVariableTemplate(getTranslation("profile.invoice.net"),
|
||||
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"),
|
||||
VaadinIcon.MONEY, "services.gross_total", "178,50 €");
|
||||
|
||||
@@ -1027,8 +1006,8 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
||||
"image");
|
||||
|
||||
panel.add(invoiceHeader, senderCompany, senderName, senderAddress, senderCity, senderEmail, senderPhone,
|
||||
invoiceNumber, servicesHeader, servicesListBlock, servicesNetBlock, servicesVatBlock,
|
||||
servicesGrossBlock, customerHeader, customerCompany, customerName, customerAddress, customerCity,
|
||||
invoiceNumber, servicesHeader, servicesListBlock, servicesNetBlock, servicesGrossBlock, customerHeader,
|
||||
customerCompany, customerName, customerAddress, customerCity,
|
||||
customerEmail, customerPhone, freeHeader, textBlock, headerBlock, dateBlock, customerBlock,
|
||||
companyBlock, amountBlock, lineBlock, imageBlock);
|
||||
|
||||
@@ -1476,7 +1455,6 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
||||
case "customer.phone" -> "Telefon des Kunden";
|
||||
case "services.list" -> "Liste aller Leistungen auf der Rechnung";
|
||||
case "services.net_total" -> "Nettosumme aller Leistungen";
|
||||
case "services.vat_total" -> "Umsatzsteuer aller Leistungen";
|
||||
case "services.gross_total" -> "Bruttosumme aller Leistungen";
|
||||
default -> "Variable: " + variable;
|
||||
};
|
||||
@@ -1530,13 +1508,6 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
||||
return getTranslation("profile.services.calculated");
|
||||
}).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
|
||||
.addColumn(
|
||||
service -> service.isMandatory() ? getTranslation("common.yes") : getTranslation("common.no"))
|
||||
@@ -1626,16 +1597,6 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
||||
calculationBasisCombo.setRequired(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
|
||||
Checkbox mandatoryCheckbox = new Checkbox(getTranslation("profile.services.mandatory"));
|
||||
mandatoryCheckbox.setValue(false);
|
||||
@@ -1644,9 +1605,6 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
||||
if (service != null) {
|
||||
nameField.setValue(service.getName());
|
||||
calculationBasisCombo.setValue(service.getCalculationBasis());
|
||||
if (service.getVatRate() != null) {
|
||||
vatRateField.setValue(service.getVatRate().multiply(new BigDecimal("100")).doubleValue());
|
||||
}
|
||||
mandatoryCheckbox.setValue(service.isMandatory());
|
||||
}
|
||||
|
||||
@@ -1679,9 +1637,6 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
||||
if (service != null) {
|
||||
nameField.setValue(service.getName());
|
||||
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
|
||||
if (service.getCalculationBasis() != null) {
|
||||
@@ -1721,7 +1676,7 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
||||
timePriceField.setVisible(initialBasis == Service.CalculationBasis.TIME);
|
||||
|
||||
formLayout.add(nameField, calculationBasisCombo, flatRatePriceField, distancePriceField, timePriceField,
|
||||
vatRateField, mandatoryCheckbox);
|
||||
mandatoryCheckbox);
|
||||
|
||||
// Action buttons
|
||||
HorizontalLayout buttonLayout = new HorizontalLayout();
|
||||
@@ -1734,7 +1689,7 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
||||
|
||||
Button saveButton = new Button(getTranslation("button.savechanges"), e -> {
|
||||
if (validateServiceForm(nameField, calculationBasisCombo, flatRatePriceField, distancePriceField,
|
||||
timePriceField, vatRateField, mandatoryCheckbox)) {
|
||||
timePriceField, mandatoryCheckbox)) {
|
||||
// Get the appropriate price based on calculation basis
|
||||
BigDecimal priceValue = BigDecimal.ZERO;
|
||||
Service.CalculationBasis selectedBasis = calculationBasisCombo.getValue();
|
||||
@@ -1748,10 +1703,8 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
||||
priceValue = new BigDecimal(timePriceField.getValue());
|
||||
}
|
||||
|
||||
BigDecimal vatRate = new BigDecimal(vatRateField.getValue()).divide(new BigDecimal("100"));
|
||||
boolean mandatory = mandatoryCheckbox.getValue();
|
||||
saveService(service, nameField.getValue(), calculationBasisCombo.getValue(), priceValue, vatRate,
|
||||
mandatory);
|
||||
saveService(service, nameField.getValue(), calculationBasisCombo.getValue(), priceValue, mandatory);
|
||||
dialog.close();
|
||||
}
|
||||
});
|
||||
@@ -1769,7 +1722,7 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
||||
*/
|
||||
private boolean validateServiceForm(TextField nameField, ComboBox<Service.CalculationBasis> calculationBasisCombo,
|
||||
NumberField flatRatePriceField, NumberField distancePriceField, NumberField timePriceField,
|
||||
NumberField vatRateField, Checkbox mandatoryCheckbox) {
|
||||
Checkbox mandatoryCheckbox) {
|
||||
boolean isValid = true;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1831,14 +1776,14 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
||||
* Save service to database
|
||||
*/
|
||||
private void saveService(Service existingService, String name, Service.CalculationBasis calculationBasis,
|
||||
BigDecimal priceValue, BigDecimal vatRate, boolean mandatory) {
|
||||
BigDecimal priceValue, boolean mandatory) {
|
||||
try {
|
||||
Service service;
|
||||
if (existingService != null) {
|
||||
service = existingService;
|
||||
service.setName(name);
|
||||
service.setCalculationBasis(calculationBasis);
|
||||
service.setVatRate(vatRate);
|
||||
service.setVatRate(Service.FIXED_VAT_RATE);
|
||||
service.setMandatory(mandatory);
|
||||
|
||||
// Set the appropriate price field based on calculation basis
|
||||
@@ -1860,8 +1805,8 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
service = new Service(currentUser.getId().toString(), name, calculationBasis, priceValue, vatRate,
|
||||
mandatory);
|
||||
service = new Service(currentUser.getId().toString(), name, calculationBasis, priceValue,
|
||||
Service.FIXED_VAT_RATE, mandatory);
|
||||
}
|
||||
|
||||
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.DeliveryStation;
|
||||
import de.assecutor.votianlt.model.Job;
|
||||
import de.assecutor.votianlt.model.JobServiceSelection;
|
||||
import de.assecutor.votianlt.model.LocationPosition;
|
||||
import de.assecutor.votianlt.model.task.BaseTask;
|
||||
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") + ":",
|
||||
formatPrice(priceResult.netAmount()), false));
|
||||
priceTable.add(createPriceRow(getTranslation("jobsummary.info.ust") + ":", formatPrice(priceResult.vatAmount()),
|
||||
false));
|
||||
priceTable.add(createPriceRow(getTranslation("jobsummary.info.gesamt") + ":",
|
||||
formatPrice(priceResult.totalAmount()), true));
|
||||
|
||||
@@ -1414,6 +1413,30 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
||||
BigDecimal netTotal = 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();
|
||||
if (serviceIds == null || serviceIds.isEmpty()) {
|
||||
// Fallback auf gespeicherten Preis
|
||||
@@ -1431,7 +1454,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
||||
continue;
|
||||
|
||||
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);
|
||||
vatTotal = vatTotal.add(price.multiply(vatRate));
|
||||
|
||||
@@ -481,7 +481,6 @@ public class CustomerInvoiceService {
|
||||
|
||||
// Calculate totals
|
||||
double netTotal = 655.00;
|
||||
double vatTotal = 124.45;
|
||||
double grossTotal = 779.45;
|
||||
|
||||
// Wrapper div
|
||||
@@ -530,14 +529,6 @@ public class CustomerInvoiceService {
|
||||
.append(String.format(java.util.Locale.GERMANY, "%,.2f €", netTotal)).append("</td>");
|
||||
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
|
||||
html.append("<tr>");
|
||||
html.append("<td style='width:55%;padding:2px 0;'></td>");
|
||||
@@ -782,9 +773,7 @@ public class CustomerInvoiceService {
|
||||
|
||||
// Get invoice data from variables
|
||||
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 vatRate = variables.getOrDefault("invoice.vat_rate", "19%");
|
||||
|
||||
// Parse services JSON from variables
|
||||
java.util.List<java.util.Map<String, String>> servicesData = new java.util.ArrayList<>();
|
||||
@@ -809,9 +798,7 @@ public class CustomerInvoiceService {
|
||||
// Header row
|
||||
html.append("<tr style='background-color:#f5f5f5;border-bottom:1px solid #cccccc;'>");
|
||||
html.append(
|
||||
"<th style='text-align:left;padding:4px 8px;font-weight:bold;width:55%;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>");
|
||||
"<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:25%;white-space:nowrap;'>Nettobetrag</th>");
|
||||
html.append("</tr>");
|
||||
@@ -821,23 +808,20 @@ public class CustomerInvoiceService {
|
||||
// Fallback: show a single row with no data
|
||||
html.append("<tr style='border-bottom:1px solid #eeeeee;'>");
|
||||
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>");
|
||||
} else {
|
||||
for (int i = 0; i < servicesData.size(); i++) {
|
||||
java.util.Map<String, String> service = servicesData.get(i);
|
||||
String name = service.getOrDefault("name", "Unbekannte Leistung");
|
||||
String serviceVatRate = service.getOrDefault("vatRate", vatRate);
|
||||
String netAmount = service.getOrDefault("netAmount", "0,00");
|
||||
|
||||
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(
|
||||
"<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>");
|
||||
html.append("<td style='text-align:right;padding:4px 8px;white-space:nowrap;'>").append(serviceVatRate)
|
||||
.append("</td>");
|
||||
html.append("<td style='text-align:right;padding:4px 8px;white-space:nowrap;'>").append(netAmount)
|
||||
html.append("<td style='text-align:right;padding:4px 8px;white-space:nowrap;width:25%;'>").append(netAmount)
|
||||
.append(" €</td>");
|
||||
html.append("</tr>");
|
||||
}
|
||||
@@ -849,9 +833,6 @@ public class CustomerInvoiceService {
|
||||
html.append("<div style='margin-top:8px;width:100%;'>");
|
||||
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
|
||||
html.append("<tr>");
|
||||
html.append("<td style='width:55%;padding:2px 0;'></td>");
|
||||
@@ -860,15 +841,6 @@ public class CustomerInvoiceService {
|
||||
.append(netTotal).append("</td>");
|
||||
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
|
||||
html.append("<tr>");
|
||||
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.dialog.title=Leistung auswählen
|
||||
addjob.services.dialog.placeholder=Leistung wählen
|
||||
addjob.services.dialog.station.placeholder=Lieferstation wählen
|
||||
addjob.services.dialog.add=Hinzufügen
|
||||
addjob.services.deliverystation=Lieferstation
|
||||
addjob.summary.title=Zusammenfassung
|
||||
addjob.summary.net=Netto
|
||||
addjob.summary.vat=Mehrwertsteuer
|
||||
@@ -937,4 +939,4 @@ adminpricetable.field.applicense=App-Nutzungslizenz
|
||||
adminpricetable.field.revenue=Umsatzbeteiligung
|
||||
adminpricetable.notification.saved=Preistabelle wurde gespeichert
|
||||
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.dialog.title=Select Service
|
||||
addjob.services.dialog.placeholder=Select service
|
||||
addjob.services.dialog.station.placeholder=Select delivery station
|
||||
addjob.services.dialog.add=Add
|
||||
addjob.services.deliverystation=Delivery Station
|
||||
addjob.summary.title=Summary
|
||||
addjob.summary.net=Net
|
||||
addjob.summary.vat=VAT
|
||||
@@ -936,4 +938,4 @@ adminpricetable.field.applicense=App Usage License
|
||||
adminpricetable.field.revenue=Revenue Participation
|
||||
adminpricetable.notification.saved=Price table has been saved
|
||||
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="amount-col">${invoiceData.netAmount}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-col">zzgl. ${invoiceData.vatRate} USt.:</td>
|
||||
<td class="amount-col">${invoiceData.vatAmount}</td>
|
||||
</tr>
|
||||
<tr class="total-row">
|
||||
<td class="label-col">Rechnungsbetrag:</td>
|
||||
<td class="amount-col">${invoiceData.totalAmount}</td>
|
||||
@@ -465,4 +461,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user