feat: Demo-Modus mit automatischer Session-Verwaltung und Rechnungsgenerator-Updates
This commit is contained in:
@@ -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.
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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("'");
|
||||
|
||||
547
src/main/java/de/assecutor/votianlt/service/DemoModeService.java
Normal file
547
src/main/java/de/assecutor/votianlt/service/DemoModeService.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=Мастер настройки
|
||||
|
||||
@@ -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
|
||||
|
||||
187
src/main/resources/templates/demo_invoice_template_default.json
Normal file
187
src/main/resources/templates/demo_invoice_template_default.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user