feat: Demo-Modus mit automatischer Session-Verwaltung und Rechnungsgenerator-Updates

This commit is contained in:
2026-03-23 18:33:48 +01:00
parent b70c45b1d5
commit e949d0c46a
28 changed files with 1292 additions and 31 deletions

View File

@@ -1,6 +1,6 @@
docker buildx build --platform linux/amd64 -t appcreationgmbh/votianlt:0.8.0 --push .
docker buildx build --platform linux/amd64 -t repository.assecutor.de/votianlt:0.9.10 --push .
docker buildx build --platform linux/amd64 -t registry.assecutor.de/votianlt:0.9.10 --push .
adsg
G8m0T3vz

Binary file not shown.

View File

@@ -165,6 +165,7 @@ window.initProfileInvoiceGenerator = function() {
var w = (el.width || 100) * zoomFactor;
var h = (el.height || 30) * zoomFactor;
var fontSize = (el.fontSize || 14) * zoomFactor;
var textAlign = el.textAlign || 'left';
if (el.type === 'line') {
ctx.strokeStyle = el.color || '#333333';
@@ -269,9 +270,17 @@ window.initProfileInvoiceGenerator = function() {
var fontWeight = (el.isStatic && !el.isCustomer) ? '' : (el.fontStyle || '');
ctx.font = (fontWeight ? fontWeight + ' ' : '') + fontSize + 'px Arial';
ctx.textBaseline = 'top';
ctx.textAlign = textAlign;
var textX = x;
if (textAlign === 'center') {
textX = x + (w / 2);
} else if (textAlign === 'right') {
textX = x + w;
}
lines.forEach(function(line) {
ctx.fillText(line, x, ty);
ctx.fillText(line, textX, ty);
ty += lineHeight;
});
}
@@ -1113,6 +1122,7 @@ window.initProfileInvoiceGenerator = function() {
heightPercent: toPercentY(el.height),
fontSize: el.fontSize,
fontStyle: el.fontStyle,
textAlign: el.textAlign,
color: el.color,
isStatic: el.isStatic,
isCustomer: el.isCustomer,

View File

@@ -361,7 +361,7 @@ vaadin-vertical-layout.admin-form-view {
.hero-panel {
position: relative;
overflow: hidden;
min-height: 340px;
min-height: 420px;
justify-content: center;
text-align: center;
border-radius: 34px;
@@ -430,6 +430,30 @@ vaadin-vertical-layout.admin-form-view {
margin-top: 0.5rem;
}
.hero-actions {
gap: 1rem;
align-items: center;
}
.hero-action-group {
gap: 0.45rem;
align-items: center;
}
.hero-actions .hero-cta,
.hero-actions .hero-demo-cta {
margin-top: 0;
}
.hero-choice-hint {
max-width: 420px;
margin: 0;
color: rgba(226, 232, 240, 0.82);
font-size: 0.95rem;
line-height: 1.55;
text-align: center;
}
.section-title {
margin: 0;
text-align: center;

View File

@@ -2,6 +2,7 @@ package de.assecutor.votianlt.config;
import de.assecutor.votianlt.model.User;
import de.assecutor.votianlt.repository.UserRepository;
import de.assecutor.votianlt.service.DemoModeService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
@@ -18,15 +19,19 @@ public class DataInitializer implements CommandLineRunner {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final DemoModeService demoModeService;
public DataInitializer(UserRepository userRepository, PasswordEncoder passwordEncoder) {
public DataInitializer(UserRepository userRepository, PasswordEncoder passwordEncoder,
DemoModeService demoModeService) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.demoModeService = demoModeService;
}
@Override
public void run(String... args) throws Exception {
initializeTestUsers();
demoModeService.ensureDemoUser();
}
private void initializeTestUsers() {

View File

@@ -0,0 +1,28 @@
package de.assecutor.votianlt.config;
import com.vaadin.flow.server.VaadinServiceInitListener;
import de.assecutor.votianlt.service.DemoModeService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class DemoSessionCleanupConfig {
private static final Logger log = LoggerFactory.getLogger(DemoSessionCleanupConfig.class);
@Bean
public VaadinServiceInitListener demoSessionCleanupListener(DemoModeService demoModeService) {
return event -> event.getSource().addSessionDestroyListener(sessionDestroyEvent -> {
try {
var wrappedSession = sessionDestroyEvent.getSession().getSession();
if (wrappedSession != null) {
demoModeService.cleanupAndReleaseIfOwned(wrappedSession.getId());
}
} catch (Exception ex) {
log.warn("Demo session destroy cleanup failed: {}", ex.getMessage(), ex);
}
});
}
}

View File

@@ -522,7 +522,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
private List<String> buildDeliverySummaryDetails(List<BaseTask> tasks) {
if (tasks == null || tasks.isEmpty()) {
return List.of(getTranslation("addjob.tab.tasks") + ": " + getTranslation("jobsummary.tasks.none"));
return new ArrayList<>(
List.of(getTranslation("addjob.tab.tasks") + ": " + getTranslation("jobsummary.tasks.none")));
}
List<String> summaries = new ArrayList<>();
@@ -533,7 +534,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
}
if (summaries.isEmpty()) {
return List.of(getTranslation("addjob.tab.tasks") + ": " + getTranslation("jobsummary.tasks.none"));
return new ArrayList<>(
List.of(getTranslation("addjob.tab.tasks") + ": " + getTranslation("jobsummary.tasks.none")));
}
return summaries;
}
@@ -651,7 +653,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
return sortVisibleTasks(station.getTasks());
}
return List.of();
return new ArrayList<>();
}
private List<BaseTask> sortVisibleTasks(List<BaseTask> tasks) {

View File

@@ -21,13 +21,12 @@ import com.vaadin.flow.server.auth.AnonymousAllowed;
import de.assecutor.votianlt.security.totp.TwoFactorService;
import de.assecutor.votianlt.repository.UserRepository;
import de.assecutor.votianlt.model.User;
import de.assecutor.votianlt.security.SessionAuthenticationService;
import de.assecutor.votianlt.service.DemoModeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import com.vaadin.flow.server.VaadinSession;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.env.Environment;
import jakarta.annotation.PostConstruct;
@@ -53,6 +52,9 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
@Autowired
private UserRepository userRepository;
@Autowired
private SessionAuthenticationService sessionAuthenticationService;
@Value("${app.security.two-factor.enabled:false}")
private boolean twoFactorEnabledGlobal;
@@ -140,6 +142,15 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
}
private void handlePasswordLogin(String username, String password) {
hideInlineFlash();
if (DemoModeService.isDemoUsername(username)) {
pendingAuth = null;
loginForm.setError(true);
showInlineFlash(getTranslation("login.demo.only.button"));
return;
}
try {
// Prüfe Benutzername/Passwort
Authentication auth = authenticationManager
@@ -158,13 +169,7 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
Notification.show(getTranslation("login.2fa.sent"), 3000, Notification.Position.BOTTOM_CENTER);
} else {
// 2FA deaktiviert: Direkt anmelden
SecurityContextHolder.getContext().setAuthentication(auth);
var vaadinSession = VaadinSession.getCurrent();
if (vaadinSession != null) {
var wrappedSession = vaadinSession.getSession();
wrappedSession.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY,
SecurityContextHolder.getContext());
}
sessionAuthenticationService.storeAuthentication(auth);
// Check if user is admin and redirect accordingly
if (auth.getAuthorities().stream()
.anyMatch(authority -> authority.getAuthority().equals("ROLE_ADMIN"))) {
@@ -196,19 +201,12 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
return;
}
// 2FA korrekt: Benutzer nun anmelden
SecurityContextHolder.getContext().setAuthentication(pendingAuth);
// Persistiere SecurityContext in der HTTP-Session, damit Vaadin/Security ihn in
// neuen Requests sieht
var vaadinSession = VaadinSession.getCurrent();
if (vaadinSession != null) {
var wrappedSession = vaadinSession.getSession();
wrappedSession.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY,
SecurityContextHolder.getContext());
}
Authentication authenticatedUser = pendingAuth;
sessionAuthenticationService.storeAuthentication(authenticatedUser);
this.pendingAuth = null;
// Full reload, damit der neue SecurityContext im UI sicher greift
// Check if user is admin and redirect accordingly
if (SecurityContextHolder.getContext().getAuthentication().getAuthorities().stream()
if (authenticatedUser.getAuthorities().stream()
.anyMatch(auth -> auth.getAuthority().equals("ROLE_ADMIN"))) {
UI.getCurrent().getPage().setLocation("/admin-dashboard");
} else {
@@ -246,4 +244,14 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
public String getPageTitle() {
return getTranslation("page.title.login");
}
private void showInlineFlash(String message) {
flashBox.setText(message);
flashBox.getStyle().set("display", "block");
}
private void hideInlineFlash() {
flashBox.setText("");
flashBox.getStyle().set("display", "none");
}
}

View File

@@ -9,6 +9,8 @@ import com.vaadin.flow.component.contextmenu.ContextMenu;
import com.vaadin.flow.component.html.*;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.notification.NotificationVariant;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
@@ -18,8 +20,12 @@ import com.vaadin.flow.router.BeforeEnterEvent;
import com.vaadin.flow.router.BeforeEnterObserver;
import com.vaadin.flow.server.auth.AnonymousAllowed;
import de.assecutor.votianlt.model.Language;
import de.assecutor.votianlt.security.SessionAuthenticationService;
import de.assecutor.votianlt.security.SecurityService;
import de.assecutor.votianlt.service.DemoModeService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import java.util.Locale;
@@ -28,10 +34,18 @@ import java.util.Locale;
public class StartView extends VerticalLayout implements BeforeEnterObserver, HasDynamicTitle {
private final SecurityService securityService;
private final DemoModeService demoModeService;
private final SessionAuthenticationService sessionAuthenticationService;
private final AuthenticationManager authenticationManager;
private final String appVersion;
public StartView(SecurityService securityService, @Value("${app.version:unknown}") String appVersion) {
public StartView(SecurityService securityService, DemoModeService demoModeService,
SessionAuthenticationService sessionAuthenticationService, AuthenticationManager authenticationManager,
@Value("${app.version:unknown}") String appVersion) {
this.securityService = securityService;
this.demoModeService = demoModeService;
this.sessionAuthenticationService = sessionAuthenticationService;
this.authenticationManager = authenticationManager;
this.appVersion = appVersion;
addClassName("landing-view");
setSizeFull();
@@ -278,11 +292,48 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha
Paragraph heroDescription = new Paragraph(getTranslation("start.hero.description"));
heroDescription.addClassName("hero-panel-text");
Button demoButton = new Button(getTranslation("start.button.demo"), event -> loginDemo());
demoButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_LARGE);
demoButton.addClassNames("hero-cta", "hero-demo-cta");
demoButton.setWidthFull();
Button ctaButton = new Button(getTranslation("cta.freetest"), event -> register());
ctaButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_LARGE);
ctaButton.addClassName("hero-cta");
ctaButton.setWidthFull();
heroSection.add(heroIcon, heroTitle, heroDescription, ctaButton);
Paragraph demoHint = new Paragraph(getTranslation("start.hero.demo.hint"));
demoHint.addClassName("hero-choice-hint");
Paragraph trialHint = new Paragraph(getTranslation("start.hero.trial.hint"));
trialHint.addClassName("hero-choice-hint");
VerticalLayout demoAction = new VerticalLayout();
demoAction.setPadding(false);
demoAction.setSpacing(false);
demoAction.setWidthFull();
demoAction.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
demoAction.addClassName("hero-action-group");
demoAction.add(demoButton, demoHint);
VerticalLayout trialAction = new VerticalLayout();
trialAction.setPadding(false);
trialAction.setSpacing(false);
trialAction.setWidthFull();
trialAction.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
trialAction.addClassName("hero-action-group");
trialAction.add(ctaButton, trialHint);
VerticalLayout heroActions = new VerticalLayout();
heroActions.setPadding(false);
heroActions.setSpacing(false);
heroActions.setWidthFull();
heroActions.setMaxWidth("420px");
heroActions.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
heroActions.addClassName("hero-actions");
heroActions.add(demoAction, trialAction);
heroSection.add(heroIcon, heroTitle, heroDescription, heroActions);
return heroSection;
}
@@ -407,6 +458,38 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha
UI.getCurrent().navigate("login");
}
private void loginDemo() {
String sessionId = sessionAuthenticationService.getCurrentSessionId().orElse(null);
if (sessionId == null) {
showDemoNotification(getTranslation("demo.start.error"), NotificationVariant.LUMO_ERROR);
return;
}
boolean prepared = false;
try {
prepared = demoModeService.tryPrepareDemoSession(sessionId);
if (!prepared) {
showDemoNotification(getTranslation("demo.session.active"), NotificationVariant.LUMO_ERROR);
return;
}
var auth = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(DemoModeService.DEMO_USERNAME, DemoModeService.DEMO_PASSWORD));
sessionAuthenticationService.storeAuthentication(auth);
UI.getCurrent().getPage().setLocation("/dashboard");
} catch (Exception ex) {
if (prepared) {
demoModeService.cleanupAndReleaseIfOwned(sessionId);
}
showDemoNotification(getTranslation("demo.start.error"), NotificationVariant.LUMO_ERROR);
}
}
private void showDemoNotification(String message, NotificationVariant variant) {
Notification notification = Notification.show(message, 4000, Notification.Position.TOP_CENTER);
notification.addThemeVariants(variant);
}
@Override
public String getPageTitle() {
return getTranslation("page.title.welcome");

View File

@@ -2,7 +2,6 @@ package de.assecutor.votianlt.pages.view;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.html.H3;
import com.vaadin.flow.component.html.Main;
import com.vaadin.flow.component.html.Span;

View File

@@ -1,6 +1,8 @@
package de.assecutor.votianlt.security;
import com.vaadin.flow.spring.security.AuthenticationContext;
import de.assecutor.votianlt.service.DemoModeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
@@ -12,12 +14,15 @@ import org.springframework.stereotype.Component;
import java.util.Optional;
@Component
@Slf4j
public class SecurityService {
private final AuthenticationContext authenticationContext;
private final DemoModeService demoModeService;
public SecurityService(AuthenticationContext authenticationContext) {
public SecurityService(AuthenticationContext authenticationContext, DemoModeService demoModeService) {
this.authenticationContext = authenticationContext;
this.demoModeService = demoModeService;
}
public Optional<UserDetails> getAuthenticatedUser() {
@@ -99,6 +104,14 @@ public class SecurityService {
}
public void logout() {
try {
String authenticatedUsername = getAuthenticatedUser().map(UserDetails::getUsername).orElse(null);
if (DemoModeService.isDemoUsername(authenticatedUsername)) {
demoModeService.cleanupCurrentSessionIfOwned();
}
} catch (Exception ex) {
log.warn("Demo logout cleanup failed: {}", ex.getMessage(), ex);
}
authenticationContext.logout();
}

View File

@@ -0,0 +1,37 @@
package de.assecutor.votianlt.security;
import com.vaadin.flow.server.VaadinSession;
import java.util.Optional;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.stereotype.Service;
@Service
public class SessionAuthenticationService {
public void storeAuthentication(Authentication authentication) {
SecurityContextHolder.getContext().setAuthentication(authentication);
VaadinSession vaadinSession = VaadinSession.getCurrent();
if (vaadinSession == null) {
return;
}
var wrappedSession = vaadinSession.getSession();
if (wrappedSession == null) {
return;
}
wrappedSession.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY,
SecurityContextHolder.getContext());
}
public Optional<String> getCurrentSessionId() {
VaadinSession vaadinSession = VaadinSession.getCurrent();
if (vaadinSession == null || vaadinSession.getSession() == null) {
return Optional.empty();
}
return Optional.ofNullable(vaadinSession.getSession().getId());
}
}

View File

@@ -358,6 +358,7 @@ public class CustomerInvoiceService {
int fontSize = element.has("fontSize") ? element.get("fontSize").asInt(14) : 14;
String color = element.has("color") ? element.get("color").asText("#333333") : "#333333";
String textAlign = element.has("textAlign") ? element.get("textAlign").asText("left") : "left";
// Convert percentages to mm (A4 is 210mm x 297mm)
double mmX = xPercent / 100.0 * 210.0;
@@ -392,6 +393,17 @@ public class CustomerInvoiceService {
} else {
// Vertically center content for other elements
htmlBuilder.append("display:flex;align-items:center;");
switch (textAlign) {
case "center":
htmlBuilder.append("justify-content:center;text-align:center;");
break;
case "right":
htmlBuilder.append("justify-content:flex-end;text-align:right;");
break;
default:
htmlBuilder.append("justify-content:flex-start;text-align:left;");
break;
}
}
}
htmlBuilder.append("'");
@@ -649,6 +661,7 @@ public class CustomerInvoiceService {
int fontSize = element.has("fontSize") ? element.get("fontSize").asInt(14) : 14;
String color = element.has("color") ? element.get("color").asText("#333333") : "#333333";
String textAlign = element.has("textAlign") ? element.get("textAlign").asText("left") : "left";
// Convert percentages to mm (A4 is 210mm x 297mm)
double mmX = xPercent / 100.0 * 210.0;
@@ -682,6 +695,17 @@ public class CustomerInvoiceService {
htmlBuilder.append("display:block;overflow:visible;padding:0;");
} else {
htmlBuilder.append("display:flex;align-items:center;");
switch (textAlign) {
case "center":
htmlBuilder.append("justify-content:center;text-align:center;");
break;
case "right":
htmlBuilder.append("justify-content:flex-end;text-align:right;");
break;
default:
htmlBuilder.append("justify-content:flex-start;text-align:left;");
break;
}
}
}
htmlBuilder.append("'");

View File

@@ -0,0 +1,547 @@
package de.assecutor.votianlt.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.assecutor.votianlt.model.AppUser;
import de.assecutor.votianlt.model.Barcode;
import de.assecutor.votianlt.model.Comment;
import de.assecutor.votianlt.model.Customer;
import de.assecutor.votianlt.model.DeliveryStation;
import de.assecutor.votianlt.model.InvoiceTemplate;
import de.assecutor.votianlt.model.Job;
import de.assecutor.votianlt.model.JobServiceSelection;
import de.assecutor.votianlt.model.JobStatus;
import de.assecutor.votianlt.model.Language;
import de.assecutor.votianlt.model.LocationPosition;
import de.assecutor.votianlt.model.Photo;
import de.assecutor.votianlt.model.Service;
import de.assecutor.votianlt.model.Signature;
import de.assecutor.votianlt.model.TaskTemplate;
import de.assecutor.votianlt.model.User;
import de.assecutor.votianlt.model.invoices.CustomerInvoice;
import de.assecutor.votianlt.model.task.BaseTask;
import de.assecutor.votianlt.pages.domain.CustomerRepository;
import de.assecutor.votianlt.repository.AppUserRepository;
import de.assecutor.votianlt.repository.BarcodeRepository;
import de.assecutor.votianlt.repository.CargoItemRepository;
import de.assecutor.votianlt.repository.CommentRepository;
import de.assecutor.votianlt.repository.CustomerInvoiceRepository;
import de.assecutor.votianlt.repository.InvoiceTemplateRepository;
import de.assecutor.votianlt.repository.JobHistoryRepository;
import de.assecutor.votianlt.repository.JobRepository;
import de.assecutor.votianlt.repository.LocationPositionRepository;
import de.assecutor.votianlt.repository.MessageRepository;
import de.assecutor.votianlt.repository.PhotoRepository;
import de.assecutor.votianlt.repository.ServiceRepository;
import de.assecutor.votianlt.repository.SignatureRepository;
import de.assecutor.votianlt.repository.TaskRepository;
import de.assecutor.votianlt.repository.TaskTemplateRepository;
import de.assecutor.votianlt.repository.UserInvoiceDataRepository;
import de.assecutor.votianlt.repository.UserRepository;
import de.assecutor.votianlt.security.SessionAuthenticationService;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.bson.types.ObjectId;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.transaction.annotation.Transactional;
@org.springframework.stereotype.Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class DemoModeService {
public static final String DEMO_USERNAME = "demo";
public static final String DEMO_PASSWORD = "demo";
private static final String DEMO_FIRST_NAME = "Demo";
private static final String DEMO_LAST_NAME = "Benutzer";
private static final String DEMO_INVOICE_TEMPLATE_RESOURCE = "templates/demo_invoice_template_default.json";
private final DemoSessionRegistry demoSessionRegistry;
private final SessionAuthenticationService sessionAuthenticationService;
private final UserRepository userRepository;
private final CustomerRepository customerRepository;
private final JobRepository jobRepository;
private final CargoItemRepository cargoItemRepository;
private final TaskRepository taskRepository;
private final JobHistoryRepository jobHistoryRepository;
private final CustomerInvoiceRepository customerInvoiceRepository;
private final ServiceRepository serviceRepository;
private final AppUserRepository appUserRepository;
private final TaskTemplateRepository taskTemplateRepository;
private final InvoiceTemplateRepository invoiceTemplateRepository;
private final UserInvoiceDataRepository userInvoiceDataRepository;
private final MessageRepository messageRepository;
private final LocationPositionRepository locationPositionRepository;
private final PhotoRepository photoRepository;
private final SignatureRepository signatureRepository;
private final BarcodeRepository barcodeRepository;
private final CommentRepository commentRepository;
private final PasswordEncoder passwordEncoder;
public static boolean isDemoUsername(String username) {
return username != null && DEMO_USERNAME.equalsIgnoreCase(username.trim());
}
public void ensureDemoUser() {
User demoUser = userRepository.findByEmail(DEMO_USERNAME).orElseGet(User::new);
applyDemoUserDefaults(demoUser);
userRepository.save(demoUser);
}
public boolean tryPrepareDemoSession(String sessionId) {
if (!demoSessionRegistry.acquire(sessionId)) {
return false;
}
try {
ensureDemoUser();
cleanupDemoOwnedData();
resetDemoUserProfile();
seedDemoData();
return true;
} catch (RuntimeException ex) {
demoSessionRegistry.release(sessionId);
throw ex;
}
}
public void cleanupCurrentSessionIfOwned() {
sessionAuthenticationService.getCurrentSessionId().ifPresent(this::cleanupAndReleaseIfOwned);
}
public void cleanupAndReleaseIfOwned(String sessionId) {
if (!demoSessionRegistry.isHeldBy(sessionId)) {
return;
}
try {
cleanupDemoOwnedData();
resetDemoUserProfile();
} catch (RuntimeException ex) {
log.warn("Demo cleanup failed for session {}: {}", sessionId, ex.getMessage(), ex);
} finally {
demoSessionRegistry.release(sessionId);
}
}
public void resetDemoUserProfile() {
User demoUser = getDemoUser();
applyDemoUserDefaults(demoUser);
userRepository.save(demoUser);
}
public void cleanupDemoOwnedData() {
Optional<User> demoUserOptional = userRepository.findByEmail(DEMO_USERNAME);
if (demoUserOptional.isEmpty()) {
return;
}
User demoUser = demoUserOptional.get();
ObjectId demoUserId = demoUser.getId();
if (demoUserId == null) {
return;
}
String demoUserIdHex = demoUserId.toHexString();
List<AppUser> demoAppUsers = appUserRepository.findByErstelltVon(demoUserId);
cleanupDemoAppUserData(demoAppUsers);
List<Job> demoJobs = jobRepository.findByCreatedBy(demoUserIdHex);
cleanupDemoJobData(demoJobs);
List<Customer> demoCustomers = customerRepository.findByOwner(demoUserId);
if (!demoCustomers.isEmpty()) {
customerRepository.deleteAll(demoCustomers);
}
serviceRepository.deleteByUserId(demoUserIdHex);
List<TaskTemplate> demoTaskTemplates = taskTemplateRepository.findByUserIdOrderByTemplateNameAsc(demoUserId);
if (!demoTaskTemplates.isEmpty()) {
taskTemplateRepository.deleteAll(demoTaskTemplates);
}
invoiceTemplateRepository.deleteByUserId(demoUserIdHex);
userInvoiceDataRepository.deleteByUserId(demoUserId);
List<CustomerInvoice> demoInvoices = customerInvoiceRepository.findByUserId(demoUserIdHex);
if (!demoInvoices.isEmpty()) {
customerInvoiceRepository.deleteAll(demoInvoices);
}
if (!demoAppUsers.isEmpty()) {
appUserRepository.deleteAll(demoAppUsers);
}
}
public void seedDemoData() {
User demoUser = getDemoUser();
List<Service> services = seedDemoServices(demoUser);
List<Customer> customers = seedDemoCustomers(demoUser);
seedDemoJobs(demoUser, customers, services);
seedDemoInvoiceTemplate(demoUser);
}
private void cleanupDemoAppUserData(List<AppUser> demoAppUsers) {
for (AppUser appUser : demoAppUsers) {
String appUserId = appUser.getIdAsString();
if (appUserId == null || appUserId.isBlank()) {
continue;
}
var messages = messageRepository.findByReceiverOrderByCreatedAtAsc(appUserId);
if (!messages.isEmpty()) {
messageRepository.deleteAll(messages);
}
var positions = locationPositionRepository.findByAppUserIdOrderByTimestampDesc(appUserId);
if (!positions.isEmpty()) {
locationPositionRepository.deleteAll(positions);
}
}
}
private void cleanupDemoJobData(List<Job> demoJobs) {
if (demoJobs.isEmpty()) {
return;
}
Set<ObjectId> stationIds = new LinkedHashSet<>();
Set<ObjectId> jobIds = new LinkedHashSet<>();
Set<String> jobIdStrings = new LinkedHashSet<>();
for (Job job : demoJobs) {
if (job == null || job.getId() == null) {
continue;
}
jobIds.add(job.getId());
jobIdStrings.add(job.getIdAsString());
if (job.getDeliveryStations() == null) {
continue;
}
for (DeliveryStation station : job.getDeliveryStations()) {
if (station != null && station.getStationId() != null) {
stationIds.add(station.getStationId());
}
}
}
Map<ObjectId, BaseTask> tasksById = new LinkedHashMap<>();
if (!stationIds.isEmpty()) {
for (BaseTask task : taskRepository.findByStationIdIn(new ArrayList<>(stationIds))) {
if (task != null && task.getId() != null) {
tasksById.put(task.getId(), task);
}
}
}
for (ObjectId jobId : jobIds) {
for (BaseTask task : taskRepository.findByJobIdOrderByTaskOrderAsc(jobId)) {
if (task != null && task.getId() != null) {
tasksById.put(task.getId(), task);
}
}
}
cleanupTaskArtifacts(tasksById.keySet());
if (!tasksById.isEmpty()) {
taskRepository.deleteAll(tasksById.values());
}
for (ObjectId jobId : jobIds) {
var cargoItems = cargoItemRepository.findByJobId(jobId);
if (!cargoItems.isEmpty()) {
cargoItemRepository.deleteAll(cargoItems);
}
jobHistoryRepository.deleteByJobId(jobId);
}
List<CustomerInvoice> invoicesToDelete = new ArrayList<>();
for (String jobId : jobIdStrings) {
customerInvoiceRepository.findByJobId(jobId).ifPresent(invoicesToDelete::add);
}
if (!invoicesToDelete.isEmpty()) {
customerInvoiceRepository.deleteAll(dedupeInvoices(invoicesToDelete));
}
jobRepository.deleteAll(demoJobs);
}
private void cleanupTaskArtifacts(Collection<ObjectId> taskIds) {
for (ObjectId taskId : taskIds) {
if (taskId == null) {
continue;
}
List<Photo> photos = photoRepository.findByTaskId(taskId);
if (!photos.isEmpty()) {
photoRepository.deleteAll(photos);
}
List<Signature> signatures = signatureRepository.findByTaskId(taskId);
if (!signatures.isEmpty()) {
signatureRepository.deleteAll(signatures);
}
List<Barcode> barcodes = barcodeRepository.findByTaskId(taskId);
if (!barcodes.isEmpty()) {
barcodeRepository.deleteAll(barcodes);
}
List<Comment> comments = commentRepository.findByTaskIdOrderByCreatedAtDesc(taskId);
if (!comments.isEmpty()) {
commentRepository.deleteAll(comments);
}
}
}
private List<CustomerInvoice> dedupeInvoices(List<CustomerInvoice> invoices) {
Map<String, CustomerInvoice> unique = new LinkedHashMap<>();
for (CustomerInvoice invoice : invoices) {
if (invoice == null || invoice.getId() == null) {
continue;
}
unique.putIfAbsent(invoice.getId(), invoice);
}
return new ArrayList<>(unique.values());
}
private List<Customer> seedDemoCustomers(User demoUser) {
ObjectId demoUserId = demoUser.getId();
Customer firstCustomer = new Customer();
firstCustomer.setTitle("Frau");
firstCustomer.setCompanyName("Demo Bau GmbH");
firstCustomer.setFirstname("Anna");
firstCustomer.setLastName("Sommer");
firstCustomer.setTelephone("030 1234567");
firstCustomer.setMail("anna.sommer@demo.invalid");
firstCustomer.setStreet("Musterstrasse");
firstCustomer.setHouseNumber("12");
firstCustomer.setZip("10115");
firstCustomer.setCity("Berlin");
firstCustomer.setCreatedBy(demoUserId);
firstCustomer.setOwner(demoUserId);
Customer secondCustomer = new Customer();
secondCustomer.setTitle("Herr");
secondCustomer.setCompanyName("Nordhandel AG");
secondCustomer.setFirstname("Lukas");
secondCustomer.setLastName("Becker");
secondCustomer.setTelephone("040 7654321");
secondCustomer.setMail("lukas.becker@demo.invalid");
secondCustomer.setStreet("Hafenallee");
secondCustomer.setHouseNumber("8");
secondCustomer.setZip("20095");
secondCustomer.setCity("Hamburg");
secondCustomer.setCreatedBy(demoUserId);
secondCustomer.setOwner(demoUserId);
return customerRepository.saveAll(List.of(firstCustomer, secondCustomer));
}
private List<Service> seedDemoServices(User demoUser) {
String demoUserId = demoUser.getId().toHexString();
Service activeService = createFlatRateService(demoUserId, "Expresszustellung", "149.00");
Service completedService = createFlatRateService(demoUserId, "Konsolidierungszustellung", "219.00");
return serviceRepository.saveAll(List.of(activeService, completedService));
}
private void seedDemoJobs(User demoUser, List<Customer> customers, List<Service> services) {
Map<String, Customer> customersByCompany = new LinkedHashMap<>();
for (Customer customer : customers) {
customersByCompany.put(customer.getCompanyName(), customer);
}
Map<String, Service> servicesByName = new LinkedHashMap<>();
for (Service service : services) {
servicesByName.put(service.getName(), service);
}
LocalDateTime now = LocalDateTime.now();
String demoUserId = demoUser.getId().toHexString();
Job activeJob = new Job();
activeJob.setJobNumber("DEMO-AKTIV-001");
activeJob.setStatus(JobStatus.IN_PROGRESS);
activeJob.setCreatedAt(now.minusDays(2));
activeJob.setUpdatedAt(now.minusHours(3));
activeJob.setCreatedBy(demoUserId);
activeJob.setDraft(false);
activeJob.setDigitalProcessing(false);
activeJob.setPickupDate(LocalDate.now().minusDays(1));
activeJob.setPickupTime(LocalTime.of(9, 30));
activeJob.setRemark("Demo-Auftrag aktiv");
activeJob.setPrice(new BigDecimal("149.00"));
applyCustomerToPickup(activeJob, customersByCompany.get("Demo Bau GmbH"));
activeJob.setCustomerSelection("Demo Bau GmbH | Anna Sommer");
activeJob.setDeliveryStations(new ArrayList<>(List.of(
createDeliveryStation("Potsdam Expresslager", "Herr", "Timo", "Kranz", "0331 444555",
"Lange Bruecke", "4", null, "14467", "Potsdam", LocalDate.now(), LocalTime.of(14, 0), 0))));
activeJob.setServiceIds(new ArrayList<>(List.of(servicesByName.get("Expresszustellung").getId())));
activeJob.setSelectedServices(new ArrayList<>(List.of(
createJobServiceSelection(servicesByName.get("Expresszustellung"), 0))));
activeJob.syncFlatDeliveryFieldsFromStations();
Job completedJob = new Job();
completedJob.setJobNumber("DEMO-ERLEDIGT-001");
completedJob.setStatus(JobStatus.COMPLETED);
completedJob.setCreatedAt(now.minusDays(8));
completedJob.setUpdatedAt(now.minusDays(5));
completedJob.setCreatedBy(demoUserId);
completedJob.setDraft(false);
completedJob.setDigitalProcessing(false);
completedJob.setPickupDate(LocalDate.now().minusDays(7));
completedJob.setPickupTime(LocalTime.of(8, 15));
completedJob.setRemark("Demo-Auftrag abgeschlossen");
completedJob.setPrice(new BigDecimal("219.00"));
applyCustomerToPickup(completedJob, customersByCompany.get("Nordhandel AG"));
completedJob.setCustomerSelection("Nordhandel AG | Lukas Becker");
completedJob.setDeliveryStations(new ArrayList<>(List.of(
createDeliveryStation("Bremen Konsolidierung", "Frau", "Mara", "Jensen", "0421 987654", "Kontorweg",
"19", null, "28195", "Bremen", LocalDate.now().minusDays(6), LocalTime.of(16, 30), 0))));
completedJob.setServiceIds(new ArrayList<>(List.of(servicesByName.get("Konsolidierungszustellung").getId())));
completedJob.setSelectedServices(new ArrayList<>(List.of(
createJobServiceSelection(servicesByName.get("Konsolidierungszustellung"), 0))));
completedJob.syncFlatDeliveryFieldsFromStations();
jobRepository.saveAll(List.of(activeJob, completedJob));
}
private Service createFlatRateService(String userId, String name, String price) {
Service service = new Service();
service.setId(new ObjectId().toHexString());
service.setUserId(userId);
service.setName(name);
service.setCalculationBasis(Service.CalculationBasis.FLAT_RATE);
service.setPrice(new BigDecimal(price));
service.setMandatory(false);
return service;
}
private JobServiceSelection createJobServiceSelection(Service service, int deliveryStationOrder) {
JobServiceSelection selection = new JobServiceSelection();
selection.setServiceId(service.getId());
selection.setDeliveryStationOrder(deliveryStationOrder);
return selection;
}
private void seedDemoInvoiceTemplate(User demoUser) {
InvoiceTemplate template = new InvoiceTemplate(demoUser.getId().toHexString(), "Einfach",
createDemoInvoiceTemplateData());
invoiceTemplateRepository.save(template);
}
private String createDemoInvoiceTemplateData() {
try {
String templateJson = new String(
new ClassPathResource(DEMO_INVOICE_TEMPLATE_RESOURCE).getInputStream().readAllBytes(),
StandardCharsets.UTF_8);
return new ObjectMapper().writeValueAsString(templateJson);
} catch (IOException ex) {
throw new IllegalStateException("Could not load demo invoice template", ex);
}
}
private void applyCustomerToPickup(Job job, Customer customer) {
if (job == null || customer == null) {
return;
}
job.setPickupCompany(customer.getCompanyName());
job.setPickupSalutation(customer.getTitle());
job.setPickupFirstName(customer.getFirstname());
job.setPickupLastName(customer.getLastName());
job.setPickupPhone(customer.getTelephone());
job.setPickupStreet(customer.getStreet());
job.setPickupHouseNumber(customer.getHouseNumber());
job.setPickupAddressAddition(customer.getAddressAddition());
job.setPickupZip(customer.getZip());
job.setPickupCity(customer.getCity());
}
private DeliveryStation createDeliveryStation(String company, String salutation, String firstName, String lastName,
String phone, String street, String houseNumber, String addressAddition, String zip, String city,
LocalDate deliveryDate, LocalTime deliveryTime, int stationOrder) {
DeliveryStation station = new DeliveryStation();
station.ensureStationId();
station.setStationOrder(stationOrder);
station.setCompany(company);
station.setSalutation(salutation);
station.setFirstName(firstName);
station.setLastName(lastName);
station.setPhone(phone);
station.setStreet(street);
station.setHouseNumber(houseNumber);
station.setAddressAddition(addressAddition);
station.setZip(zip);
station.setCity(city);
station.setDeliveryDate(deliveryDate);
station.setDeliveryTime(deliveryTime);
station.setTasks(new ArrayList<>());
return station;
}
private User getDemoUser() {
return userRepository.findByEmail(DEMO_USERNAME)
.orElseThrow(() -> new IllegalStateException("Demo user does not exist"));
}
private void applyDemoUserDefaults(User demoUser) {
LocalDateTime now = LocalDateTime.now();
if (demoUser.getCreatedAt() == null) {
demoUser.setCreatedAt(now);
}
demoUser.setEmail(DEMO_USERNAME);
demoUser.setPassword(passwordEncoder.encode(DEMO_PASSWORD));
demoUser.setFirstname(DEMO_FIRST_NAME);
demoUser.setName(DEMO_LAST_NAME);
demoUser.setTitle(null);
demoUser.setCompany(null);
demoUser.setCompanyAddition(null);
demoUser.setStreet(null);
demoUser.setHouseNumber(null);
demoUser.setAddressAddition(null);
demoUser.setZip(null);
demoUser.setCity(null);
demoUser.setDiffInvoiceAddress(false);
demoUser.setInvCompany(null);
demoUser.setInvCompanyAddition(null);
demoUser.setInvFirstname(null);
demoUser.setInvLastname(null);
demoUser.setInvStreet(null);
demoUser.setInvHouseNumber(null);
demoUser.setInvAddressAddition(null);
demoUser.setInvZip(null);
demoUser.setInvCity(null);
demoUser.setPhone(null);
demoUser.setPhone2(null);
demoUser.setFax(null);
demoUser.setPasswordCode(null);
demoUser.setPasswordTimestamp(null);
demoUser.setIsActivated((byte) 1);
demoUser.setIsEmailConfirmed((byte) 1);
demoUser.setRoles(Set.of("USER"));
demoUser.setDigitalProcessingEnabled(true);
demoUser.setLocationTrackingEnabled(true);
demoUser.setTwoFactorEnabled(false);
demoUser.setLanguage(Language.DE);
demoUser.setUpdatedAt(now);
}
}

View File

@@ -0,0 +1,40 @@
package de.assecutor.votianlt.service;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import org.springframework.stereotype.Service;
@Service
public class DemoSessionRegistry {
private final AtomicReference<String> activeSessionId = new AtomicReference<>();
public synchronized boolean acquire(String sessionId) {
if (sessionId == null || sessionId.isBlank()) {
return false;
}
String current = activeSessionId.get();
if (current == null || Objects.equals(current, sessionId)) {
activeSessionId.set(sessionId);
return true;
}
return false;
}
public boolean isHeldBy(String sessionId) {
return sessionId != null && sessionId.equals(activeSessionId.get());
}
public synchronized void release(String sessionId) {
if (isHeldBy(sessionId)) {
activeSessionId.set(null);
}
}
public Optional<String> getActiveSessionId() {
return Optional.ofNullable(activeSessionId.get());
}
}

View File

@@ -806,11 +806,17 @@ register.notification.failed=Registrierung fehlgeschlagen: {0}
# Start Page
start.title=VotianLT - Ihr digitaler Transportpartner
start.button.login=Anmelden
start.button.demo=Demo
start.button.register=Registrieren
login.demo.only.button=Der Demo-Zugang ist nur über den Demo-Button auf der Startseite verfügbar.
demo.session.active=Der Demo-Modus wird bereits von einem anderen Nutzer verwendet.
demo.start.error=Der Demo-Modus konnte nicht gestartet werden.
start.button.createorder=Auftragserstellung
start.button.notifications=Benachrichtigungen
start.button.nonotifications=Keine neuen Benachrichtigungen
start.hero.description=Für Solo-Selbstständige und Kleinunternehmer im Transportgewerbe - volldigital und aus einem Guss. Konzentrieren Sie sich auf Ihr Geschäft, wir kümmern uns um die Büroarbeit.
start.hero.demo.hint=Demo startet sofort mit vorbereiteten Beispieldaten.
start.hero.trial.hint="Jetzt kostenlos testen" erstellt Ihren eigenen Account für den kostenlosen Probemonat.
start.system.title=Das System
start.system.intro=Für Solo-Selbstständige und Kleinunternehmer im Transportgewerbe ist von entscheidender Bedeutung, dass sie sich in erster Linie auf ihr eigentliches Geschäft konzentrieren können: Kunden gewinnen und Waren von A nach B liefern.
start.feature.setup.title=Einrichtungsassistent

View File

@@ -728,11 +728,17 @@ register.notification.success=Registreerimine \u00f5nnestus. Palun logige sisse.
register.notification.failed=Registreerimine eba\u00f5nnestus: {0}
start.title=VotianLT - Teie digitaalne transpordipartner
start.button.login=Logi sisse
start.button.demo=Demo
start.button.register=Registreeru
login.demo.only.button=Demo ligipääs on võimalik ainult avalehe Demo nupu kaudu.
demo.session.active=Demorežiimi kasutab juba teine kasutaja.
demo.start.error=Demorežiimi ei õnnestunud käivitada.
start.button.createorder=Tellimuse loomine
start.button.notifications=Teavitused
start.button.nonotifications=Uusi teavitusi pole
start.hero.description=\u00dcksikettev\u00f5tjatele ja v\u00e4ikeettev\u00f5tjatele transpordisektoris \u2013 t\u00e4ielikult digitaalne ja terviklik. Keskenduge oma \u00e4rile, meie hoolitseme kontoritöö eest.
start.hero.demo.hint=Demo k\u00e4ivitub kohe ettevalmistatud n\u00e4idisandmetega.
start.hero.trial.hint="Proovi kohe tasuta" loob sinu isikliku konto tasuta proovikuuks.
start.system.title=S\u00fcsteem
start.system.intro=\u00dcksikettev\u00f5tjate ja v\u00e4ikeettev\u00f5tjate jaoks transpordisektoris on otsustava t\u00e4htsusega, et nad saaksid keskenduda oma p\u00f5hitegevusele: klientide v\u00f5itmine ja kaupade kohaletoimetamine punktist A punkti B.
start.feature.setup.title=Seadistusabi

View File

@@ -806,11 +806,17 @@ register.notification.failed=Registration failed: {0}
# Start Page
start.title=VotianLT - Your Digital Transport Partner
start.button.login=Log In
start.button.demo=Demo
start.button.register=Register
login.demo.only.button=Demo access is only available through the Demo button on the start page.
demo.session.active=Demo mode is already being used by another user.
demo.start.error=Demo mode could not be started.
start.button.createorder=Create Job
start.button.notifications=Notifications
start.button.nonotifications=No new notifications
start.hero.description=For solo self-employed and small business owners in the transport industry - fully digital and all-in-one. Focus on your business, we take care of the paperwork.
start.hero.demo.hint=Demo starts immediately with prepared sample data.
start.hero.trial.hint="Try now for free" creates your own account for the free trial month.
start.system.title=The System
start.system.intro=For solo self-employed and small business owners in the transport industry, it is crucial that they can primarily focus on their actual business: winning customers and delivering goods from A to B.
start.feature.setup.title=Setup Wizard

View File

@@ -806,11 +806,17 @@ register.notification.failed=El registro ha fallado: {0}
# Start Page
start.title=VotianLT - Su socio digital de transporte
start.button.login=Iniciar sesi\u00f3n
start.button.demo=Demo
start.button.register=Registrarse
login.demo.only.button=El acceso demo solo est\u00e1 disponible mediante el bot\u00f3n Demo de la p\u00e1gina de inicio.
demo.session.active=Otro usuario ya est\u00e1 utilizando el modo demo.
demo.start.error=No se pudo iniciar el modo demo.
start.button.createorder=Crear pedido
start.button.notifications=Notificaciones
start.button.nonotifications=No hay nuevas notificaciones
start.hero.description=Para aut\u00f3nomos y peque\u00f1os empresarios del sector del transporte - totalmente digital y de una sola pieza. Conc\u00e9ntrese en su negocio, nosotros nos encargamos del trabajo de oficina.
start.hero.demo.hint=La demo se inicia inmediatamente con datos de ejemplo preparados.
start.hero.trial.hint="Probar gratis ahora" crea su propia cuenta para el mes de prueba gratuito.
start.system.title=El sistema
start.system.intro=Para aut\u00f3nomos y peque\u00f1os empresarios del sector del transporte es de vital importancia poder concentrarse en su negocio principal: captar clientes y entregar mercanc\u00edas de A a B.
start.feature.setup.title=Asistente de configuraci\u00f3n

View File

@@ -806,11 +806,17 @@ register.notification.failed=\u00c9chec de l'inscription : {0}
# Start Page
start.title=VotianLT - Votre partenaire de transport num\u00e9rique
start.button.login=Se connecter
start.button.demo=Demo
start.button.register=S'inscrire
login.demo.only.button=L'acc\u00e8s d\u00e9mo est disponible uniquement via le bouton Demo de la page d'accueil.
demo.session.active=Le mode d\u00e9mo est d\u00e9j\u00e0 utilis\u00e9 par un autre utilisateur.
demo.start.error=Impossible de d\u00e9marrer le mode d\u00e9mo.
start.button.createorder=Cr\u00e9ation de mission
start.button.notifications=Notifications
start.button.nonotifications=Aucune nouvelle notification
start.hero.description=Pour les travailleurs ind\u00e9pendants et les petites entreprises du secteur du transport - enti\u00e8rement num\u00e9rique et int\u00e9gr\u00e9. Concentrez-vous sur votre activit\u00e9, nous nous occupons de la paperasse.
start.hero.demo.hint=La d\u00e9mo d\u00e9marre imm\u00e9diatement avec des donn\u00e9es d'exemple pr\u00e9par\u00e9es.
start.hero.trial.hint="Tester gratuitement maintenant" cr\u00e9e votre propre compte pour le mois d'essai gratuit.
start.system.title=Le syst\u00e8me
start.system.intro=Pour les travailleurs ind\u00e9pendants et les petites entreprises du secteur du transport, il est essentiel de pouvoir se concentrer en priorit\u00e9 sur leur activit\u00e9 principale : gagner des clients et livrer des marchandises de A \u00e0 B.
start.feature.setup.title=Assistant de configuration

View File

@@ -806,11 +806,17 @@ register.notification.failed=Registracija nepavyko: {0}
# Start Page
start.title=VotianLT - Jūsų skaitmeninis transporto partneris
start.button.login=Prisijungti
start.button.demo=Demo
start.button.register=Registruotis
login.demo.only.button=Demo prieiga galima tik per prad\u017eios puslapio mygtuk\u0105 „Demo“.
demo.session.active=Demo re\u017eimu jau naudojasi kitas vartotojas.
demo.start.error=Nepavyko paleisti demo re\u017eimo.
start.button.createorder=Užsakymo kūrimas
start.button.notifications=Pranešimai
start.button.nonotifications=Nėra naujų pranešimų
start.hero.description=Individualiems verslininkams ir smulkaus verslo savininkams transporto sektoriuje visiškai skaitmeninis ir vientisas sprendimas. Sutelkite dėmesį į savo verslą, mes pasirūpinsime biuro darbu.
start.hero.demo.hint=Demo režimas iš karto paleidžiamas su paruoštais pavyzdiniais duomenimis.
start.hero.trial.hint=„Išbandyti nemokamai dabar“ sukuria jūsų paskyrą nemokamam bandomajam mėnesiui.
start.system.title=Sistema
start.system.intro=Individualiems verslininkams ir smulkaus verslo savininkams transporto sektoriuje labai svarbu, kad jie galėtų visų pirma sutelkti dėmesį į savo pagrindinį verslą: klientų pritraukimą ir prekių pristatymą iš taško A į tašką B.
start.feature.setup.title=Sąrankos vedlys

View File

@@ -806,11 +806,17 @@ register.notification.failed=Reģistrācija neizdevās: {0}
# Start Page
start.title=VotianLT - Jūsu digitālais transporta partneris
start.button.login=Pieteikties
start.button.demo=Demo
start.button.register=Reģistrēties
login.demo.only.button=Demonstrācijas piekļuve ir pieejama tikai caur sākumlapas pogu "Demo".
demo.session.active=Demonstrācijas režīmu jau izmanto cits lietotājs.
demo.start.error=Neizdevās palaist demonstrācijas režīmu.
start.button.createorder=Izveidot uzdevumu
start.button.notifications=Paziņojumi
start.button.nonotifications=Nav jaunu paziņojumu
start.hero.description=Individuālajiem uzņēmējiem un mazajiem uzņēmumiem transporta nozarē \u2013 pilnībā digitāli un no viena avota. Koncentrējieties uz savu biznesu, mēs parūpēsimies par biroja darbu.
start.hero.demo.hint=Demo režīms tiek palaists uzreiz ar sagatavotiem parauga datiem.
start.hero.trial.hint=“Izmēģināt bez maksas tagad” izveido jūsu kontu bezmaksas izmēģinājuma mēnesim.
start.system.title=Sistēma
start.system.intro=Individuālajiem uzņēmējiem un mazajiem uzņēmumiem transporta nozarē ir būtiski svarīgi, lai viņi varētu koncentrēties uz savu pamatdarbību: klientu piesaisti un preču piegādi no A uz B.
start.feature.setup.title=Iestatīšanas palīgs

View File

@@ -806,11 +806,17 @@ register.notification.failed=Rejestracja nie powiod\u0142a si\u0119: {0}
# Start Page
start.title=VotianLT - Tw\u00f3j cyfrowy partner transportowy
start.button.login=Zaloguj si\u0119
start.button.demo=Demo
start.button.register=Zarejestruj si\u0119
login.demo.only.button=Dost\u0119p demo jest dost\u0119pny wy\u0142\u0105cznie przez przycisk Demo na stronie startowej.
demo.session.active=Tryb demo jest ju\u017c u\u017cywany przez innego u\u017cytkownika.
demo.start.error=Nie uda\u0142o si\u0119 uruchomi\u0107 trybu demo.
start.button.createorder=Tworzenie zlece\u0144
start.button.notifications=Powiadomienia
start.button.nonotifications=Brak nowych powiadomie\u0144
start.hero.description=Dla samozatrudnionych i ma\u0142ych przedsi\u0119biorc\u00f3w w bran\u017cy transportowej - w pe\u0142ni cyfrowo i kompleksowo. Skoncentruj si\u0119 na swoim biznesie, a my zajmiemy si\u0119 prac\u0105 biurow\u0105.
start.hero.demo.hint=Demo uruchamia si\u0119 od razu z przygotowanymi danymi przyk\u0142adowymi.
start.hero.trial.hint="Testuj teraz za darmo" tworzy Twoje w\u0142asne konto na bezp\u0142atny miesi\u0105c pr\u00f3bny.
start.system.title=System
start.system.intro=Dla samozatrudnionych i ma\u0142ych przedsi\u0119biorc\u00f3w w bran\u017cy transportowej kluczowe jest, aby mogli skupi\u0107 si\u0119 przede wszystkim na swoim w\u0142a\u015bciwym biznesie: pozyskiwaniu klient\u00f3w i dostarczaniu towar\u00f3w z punktu A do punktu B.
start.feature.setup.title=Kreator konfiguracji

View File

@@ -806,11 +806,17 @@ register.notification.failed=Регистрация не удалась: {0}
# Start Page
start.title=VotianLT - Ваш цифровой транспортный партнёр
start.button.login=Войти
start.button.demo=Demo
start.button.register=Зарегистрироваться
login.demo.only.button=Демо-доступ доступен только через кнопку Demo на стартовой странице.
demo.session.active=Демо-режим уже используется другим пользователем.
demo.start.error=Не удалось запустить демо-режим.
start.button.createorder=Создание заказа
start.button.notifications=Уведомления
start.button.nonotifications=Нет новых уведомлений
start.hero.description=Для индивидуальных предпринимателей и малого бизнеса в транспортной отрасли \u2013 полностью цифровое и комплексное решение. Сосредоточьтесь на вашем бизнесе, а мы позаботимся о бумажной работе.
start.hero.demo.hint=Демо-режим запускается сразу с подготовленными примерными данными.
start.hero.trial.hint="Попробовать бесплатно сейчас" создаёт ваш собственный аккаунт для бесплатного пробного месяца.
start.system.title=Система
start.system.intro=Для индивидуальных предпринимателей и малого бизнеса в транспортной отрасли крайне важно сосредоточиться прежде всего на своём основном деле: привлечении клиентов и доставке товаров из пункта А в пункт Б.
start.feature.setup.title=Мастер настройки

View File

@@ -806,11 +806,17 @@ register.notification.failed=Kay\u0131t ba\u015far\u0131s\u0131z: {0}
# Start Page
start.title=VotianLT - Dijital Ta\u015f\u0131mac\u0131l\u0131k Orta\u011f\u0131n\u0131z
start.button.login=Giri\u015f Yap
start.button.demo=Demo
start.button.register=Kay\u0131t Ol
login.demo.only.button=Demo eri\u015fimi yaln\u0131zca ba\u015flang\u0131\u00e7 sayfas\u0131ndaki Demo d\u00fc\u011fmesi \u00fczerinden kullan\u0131labilir.
demo.session.active=Demo modu zaten ba\u015fka bir kullan\u0131c\u0131 taraf\u0131ndan kullan\u0131l\u0131yor.
demo.start.error=Demo modu ba\u015flat\u0131lamad\u0131.
start.button.createorder=\u0130\u015f Olu\u015ftur
start.button.notifications=Bildirimler
start.button.nonotifications=Yeni bildirim yok
start.hero.description=Ta\u015f\u0131mac\u0131l\u0131k sekt\u00f6r\u00fcndeki bireysel giri\u015fimciler ve k\u00fc\u00e7\u00fck i\u015fletme sahipleri i\u00e7in - tamamen dijital ve b\u00fct\u00fcnle\u015fik. \u0130\u015finize odaklan\u0131n, b\u00fcro i\u015flerini biz halledelim.
start.hero.demo.hint=Demo, \u00f6nceden haz\u0131rlanm\u0131\u015f \u00f6rnek verilerle hemen ba\u015flar.
start.hero.trial.hint="Hemen \u00fccretsiz deneyin" \u00fccretsiz deneme ay\u0131 i\u00e7in kendi hesab\u0131n\u0131z\u0131 olu\u015fturur.
start.system.title=Sistem
start.system.intro=Ta\u015f\u0131mac\u0131l\u0131k sekt\u00f6r\u00fcndeki bireysel giri\u015fimciler ve k\u00fc\u00e7\u00fck i\u015fletme sahipleri i\u00e7in as\u0131l i\u015flerine odaklanabilmeleri b\u00fcy\u00fck \u00f6nem ta\u015f\u0131maktad\u0131r: m\u00fc\u015fteri kazanmak ve mallar\u0131 A'dan B'ye ta\u015f\u0131mak.
start.feature.setup.title=Kurulum Sihirbaz\u0131

View File

@@ -0,0 +1,187 @@
{
"elements": [
{
"id": "demo-title",
"type": "header",
"text": "Rechnung",
"xPercent": "7.56",
"yPercent": "6.53",
"widthPercent": "28.07",
"heightPercent": "4.04",
"fontSize": 24,
"fontStyle": "bold",
"color": "#111111"
},
{
"id": "demo-number-label",
"type": "text",
"text": "Rechnungsnr.",
"xPercent": "67.06",
"yPercent": "7.48",
"widthPercent": "17.98",
"heightPercent": "2.97",
"fontSize": 11,
"fontStyle": "bold",
"color": "#111111"
},
{
"id": "demo-number-value",
"type": "text",
"text": "000000",
"xPercent": "84.03",
"yPercent": "7.48",
"widthPercent": "10.08",
"heightPercent": "2.97",
"fontSize": 11,
"fontStyle": "normal",
"color": "#111111",
"isStatic": true,
"variable": "invoice.number"
},
{
"id": "demo-date-label",
"type": "text",
"text": "Datum",
"xPercent": "67.06",
"yPercent": "11.52",
"widthPercent": "17.98",
"heightPercent": "2.97",
"fontSize": 11,
"fontStyle": "bold",
"color": "#111111"
},
{
"id": "demo-date-value",
"type": "text",
"text": "2026-01-01",
"xPercent": "84.03",
"yPercent": "11.52",
"widthPercent": "10.08",
"heightPercent": "2.97",
"fontSize": 11,
"fontStyle": "normal",
"color": "#111111",
"isStatic": true,
"variable": "invoice.date"
},
{
"id": "demo-sender",
"type": "text",
"text": null,
"xPercent": "7.56",
"yPercent": "16.51",
"widthPercent": "30.08",
"heightPercent": "2.97",
"fontSize": 12,
"fontStyle": "bold",
"color": "#111111",
"isStatic": true,
"variable": "masterdata.contact_name"
},
{
"id": "demo-customer",
"type": "customer",
"text": "Kundenname\nStraße Nr.\nPLZ Ort",
"xPercent": "7.56",
"yPercent": "25.06",
"widthPercent": "33.95",
"heightPercent": "12.00",
"fontSize": 12,
"fontStyle": "normal",
"color": "#111111"
},
{
"id": "demo-services",
"type": "text",
"text": "Leistungen",
"xPercent": "7.56",
"yPercent": "42.04",
"widthPercent": "85.04",
"heightPercent": "28.03",
"fontSize": 11,
"fontStyle": "normal",
"color": "#111111",
"isStatic": true,
"variable": "services.list"
},
{
"id": "demo-net-label",
"type": "text",
"text": "Netto",
"xPercent": "67.06",
"yPercent": "73.99",
"widthPercent": "11.93",
"heightPercent": "2.97",
"fontSize": 11,
"fontStyle": "bold",
"color": "#111111"
},
{
"id": "demo-net-value",
"type": "text",
"text": "0,00 €",
"xPercent": "80.00",
"yPercent": "73.99",
"widthPercent": "13.95",
"heightPercent": "2.97",
"fontSize": 11,
"fontStyle": "normal",
"color": "#111111",
"isStatic": true,
"variable": "invoice.net_total"
},
{
"id": "demo-total-label",
"type": "text",
"text": "Gesamt",
"xPercent": "67.06",
"yPercent": "78.98",
"widthPercent": "11.93",
"heightPercent": "3.44",
"fontSize": 13,
"fontStyle": "bold",
"color": "#111111"
},
{
"id": "demo-total-value",
"type": "text",
"text": "0,00 €",
"xPercent": "80.00",
"yPercent": "78.98",
"widthPercent": "13.95",
"heightPercent": "3.44",
"fontSize": 13,
"fontStyle": "bold",
"color": "#111111",
"isStatic": true,
"variable": "invoice.gross_total"
},
{
"id": "demo-template-note",
"type": "text",
"text": "Hinweis: Dieses Rechnungstemplate kann vom Benutzer ",
"xPercent": "0.00",
"yPercent": "20.78",
"widthPercent": "100.00",
"heightPercent": "4.51",
"fontSize": 20,
"fontStyle": "normal",
"textAlign": "center",
"color": "#555555"
},
{
"id": "element-1",
"type": "text",
"text": "frei angepasst werden.",
"xPercent": "31.93",
"yPercent": "24.94",
"widthPercent": "25.21",
"heightPercent": "3.56",
"fontSize": 20,
"color": "#333333",
"isStatic": false,
"isCustomer": false,
"variable": null
}
]
}

View File

@@ -0,0 +1,165 @@
package de.assecutor.votianlt.service;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import de.assecutor.votianlt.model.Job;
import de.assecutor.votianlt.model.JobStatus;
import de.assecutor.votianlt.model.User;
import de.assecutor.votianlt.model.InvoiceTemplate;
import de.assecutor.votianlt.pages.domain.CustomerRepository;
import de.assecutor.votianlt.repository.AppUserRepository;
import de.assecutor.votianlt.repository.BarcodeRepository;
import de.assecutor.votianlt.repository.CargoItemRepository;
import de.assecutor.votianlt.repository.CommentRepository;
import de.assecutor.votianlt.repository.CustomerInvoiceRepository;
import de.assecutor.votianlt.repository.InvoiceTemplateRepository;
import de.assecutor.votianlt.repository.JobHistoryRepository;
import de.assecutor.votianlt.repository.JobRepository;
import de.assecutor.votianlt.repository.LocationPositionRepository;
import de.assecutor.votianlt.repository.MessageRepository;
import de.assecutor.votianlt.repository.PhotoRepository;
import de.assecutor.votianlt.repository.ServiceRepository;
import de.assecutor.votianlt.repository.SignatureRepository;
import de.assecutor.votianlt.repository.TaskRepository;
import de.assecutor.votianlt.repository.TaskTemplateRepository;
import de.assecutor.votianlt.repository.UserInvoiceDataRepository;
import de.assecutor.votianlt.repository.UserRepository;
import de.assecutor.votianlt.security.SessionAuthenticationService;
import java.util.List;
import java.util.Optional;
import org.bson.types.ObjectId;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.crypto.password.PasswordEncoder;
@ExtendWith(MockitoExtension.class)
class DemoModeServiceTest {
@Mock
private SessionAuthenticationService sessionAuthenticationService;
@Mock
private UserRepository userRepository;
@Mock
private CustomerRepository customerRepository;
@Mock
private JobRepository jobRepository;
@Mock
private CargoItemRepository cargoItemRepository;
@Mock
private TaskRepository taskRepository;
@Mock
private JobHistoryRepository jobHistoryRepository;
@Mock
private CustomerInvoiceRepository customerInvoiceRepository;
@Mock
private ServiceRepository serviceRepository;
@Mock
private AppUserRepository appUserRepository;
@Mock
private TaskTemplateRepository taskTemplateRepository;
@Mock
private InvoiceTemplateRepository invoiceTemplateRepository;
@Mock
private UserInvoiceDataRepository userInvoiceDataRepository;
@Mock
private MessageRepository messageRepository;
@Mock
private LocationPositionRepository locationPositionRepository;
@Mock
private PhotoRepository photoRepository;
@Mock
private SignatureRepository signatureRepository;
@Mock
private BarcodeRepository barcodeRepository;
@Mock
private CommentRepository commentRepository;
@Mock
private PasswordEncoder passwordEncoder;
@Captor
private ArgumentCaptor<List<Job>> jobsCaptor;
@Captor
private ArgumentCaptor<List<de.assecutor.votianlt.model.Service>> servicesCaptor;
@Captor
private ArgumentCaptor<InvoiceTemplate> invoiceTemplateCaptor;
private DemoSessionRegistry demoSessionRegistry;
private DemoModeService demoModeService;
@BeforeEach
void setUp() {
demoSessionRegistry = new DemoSessionRegistry();
demoModeService = new DemoModeService(demoSessionRegistry, sessionAuthenticationService, userRepository,
customerRepository, jobRepository, cargoItemRepository, taskRepository, jobHistoryRepository,
customerInvoiceRepository, serviceRepository, appUserRepository, taskTemplateRepository,
invoiceTemplateRepository, userInvoiceDataRepository, messageRepository, locationPositionRepository,
photoRepository, signatureRepository, barcodeRepository, commentRepository, passwordEncoder);
}
@Test
void tryPrepareDemoSessionSeedsTwoDemoJobs() {
User demoUser = new User();
demoUser.setId(new ObjectId());
when(userRepository.findByEmail(DemoModeService.DEMO_USERNAME)).thenReturn(Optional.of(demoUser));
when(passwordEncoder.encode(DemoModeService.DEMO_PASSWORD)).thenReturn("encoded-demo");
when(customerRepository.findByOwner(demoUser.getId())).thenReturn(List.of());
when(jobRepository.findByCreatedBy(demoUser.getId().toHexString())).thenReturn(List.of());
when(appUserRepository.findByErstelltVon(demoUser.getId())).thenReturn(List.of());
when(taskTemplateRepository.findByUserIdOrderByTemplateNameAsc(demoUser.getId())).thenReturn(List.of());
when(customerInvoiceRepository.findByUserId(demoUser.getId().toHexString())).thenReturn(List.of());
when(customerRepository.saveAll(anyList())).thenAnswer(invocation -> invocation.getArgument(0));
when(serviceRepository.saveAll(anyList())).thenAnswer(invocation -> invocation.getArgument(0));
when(jobRepository.saveAll(anyList())).thenAnswer(invocation -> invocation.getArgument(0));
boolean prepared = demoModeService.tryPrepareDemoSession("demo-session");
assertThat(prepared).isTrue();
verify(serviceRepository).saveAll(servicesCaptor.capture());
assertThat(servicesCaptor.getValue()).hasSize(2);
verify(invoiceTemplateRepository).save(invoiceTemplateCaptor.capture());
assertThat(invoiceTemplateCaptor.getValue().getUserId()).isEqualTo(demoUser.getId().toHexString());
assertThat(invoiceTemplateCaptor.getValue().getTemplateData())
.contains("elements")
.contains("services.list")
.contains("invoice.gross_total")
.contains("frei angepasst werden")
.contains("textAlign")
.contains("center")
.contains("fontSize")
.contains("20");
verify(jobRepository).saveAll(jobsCaptor.capture());
assertThat(jobsCaptor.getValue()).hasSize(2);
assertThat(jobsCaptor.getValue()).extracting(Job::getJobNumber)
.containsExactly("DEMO-AKTIV-001", "DEMO-ERLEDIGT-001");
assertThat(jobsCaptor.getValue()).extracting(Job::getStatus)
.containsExactly(JobStatus.IN_PROGRESS, JobStatus.COMPLETED);
assertThat(jobsCaptor.getValue()).allSatisfy(job -> {
assertThat(job.getServiceIds()).hasSize(1);
assertThat(job.getSelectedServices()).hasSize(1);
assertThat(job.getSelectedServices().get(0).getDeliveryStationOrder()).isEqualTo(0);
assertThat(job.getSelectedServices().get(0).getServiceId()).isEqualTo(job.getServiceIds().get(0));
});
assertThat(demoSessionRegistry.isHeldBy("demo-session")).isTrue();
}
@Test
void cleanupAndReleaseIfOwnedSkipsForeignSessions() {
demoSessionRegistry.acquire("active-session");
demoModeService.cleanupAndReleaseIfOwned("other-session");
verify(userRepository, never()).findByEmail(anyString());
assertThat(demoSessionRegistry.isHeldBy("active-session")).isTrue();
}
}

View File

@@ -0,0 +1,29 @@
package de.assecutor.votianlt.service;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
class DemoSessionRegistryTest {
private final DemoSessionRegistry registry = new DemoSessionRegistry();
@Test
void acquireAllowsSameSessionButBlocksOtherSessions() {
assertThat(registry.acquire("session-a")).isTrue();
assertThat(registry.acquire("session-a")).isTrue();
assertThat(registry.acquire("session-b")).isFalse();
assertThat(registry.isHeldBy("session-a")).isTrue();
assertThat(registry.isHeldBy("session-b")).isFalse();
}
@Test
void releaseFreesLockForNextSession() {
assertThat(registry.acquire("session-a")).isTrue();
registry.release("session-a");
assertThat(registry.acquire("session-b")).isTrue();
assertThat(registry.isHeldBy("session-b")).isTrue();
}
}