diff --git a/README.md b/README.md index 38096f5..9c6b078 100644 --- a/README.md +++ b/README.md @@ -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 \ No newline at end of file diff --git a/src/main/bundles/dev.bundle b/src/main/bundles/dev.bundle index 4dc9e68..62da9a8 100644 Binary files a/src/main/bundles/dev.bundle and b/src/main/bundles/dev.bundle differ diff --git a/src/main/frontend/invoice-generator/profile-invoice-generator.js b/src/main/frontend/invoice-generator/profile-invoice-generator.js index fb81014..6d6a64a 100644 --- a/src/main/frontend/invoice-generator/profile-invoice-generator.js +++ b/src/main/frontend/invoice-generator/profile-invoice-generator.js @@ -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, diff --git a/src/main/frontend/themes/votian-modern/styles.css b/src/main/frontend/themes/votian-modern/styles.css index 2b4b0c4..7cdc5b7 100644 --- a/src/main/frontend/themes/votian-modern/styles.css +++ b/src/main/frontend/themes/votian-modern/styles.css @@ -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; diff --git a/src/main/java/de/assecutor/votianlt/config/DataInitializer.java b/src/main/java/de/assecutor/votianlt/config/DataInitializer.java index b50d459..d57ce2f 100644 --- a/src/main/java/de/assecutor/votianlt/config/DataInitializer.java +++ b/src/main/java/de/assecutor/votianlt/config/DataInitializer.java @@ -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() { diff --git a/src/main/java/de/assecutor/votianlt/config/DemoSessionCleanupConfig.java b/src/main/java/de/assecutor/votianlt/config/DemoSessionCleanupConfig.java new file mode 100644 index 0000000..6bd5499 --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/config/DemoSessionCleanupConfig.java @@ -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); + } + }); + } +} diff --git a/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java b/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java index 3343bcd..c88e2ad 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java @@ -522,7 +522,8 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has private List buildDeliverySummaryDetails(List 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 summaries = new ArrayList<>(); @@ -533,7 +534,8 @@ public class JobSummaryView extends Main implements HasUrlParameter, 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, Has return sortVisibleTasks(station.getTasks()); } - return List.of(); + return new ArrayList<>(); } private List sortVisibleTasks(List tasks) { diff --git a/src/main/java/de/assecutor/votianlt/pages/view/LoginView.java b/src/main/java/de/assecutor/votianlt/pages/view/LoginView.java index af0c4ea..2961bd3 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/LoginView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/LoginView.java @@ -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"); + } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/StartView.java b/src/main/java/de/assecutor/votianlt/pages/view/StartView.java index 27f57e1..a5745c9 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/StartView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/StartView.java @@ -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"); diff --git a/src/main/java/de/assecutor/votianlt/pages/view/UserMessagesView.java b/src/main/java/de/assecutor/votianlt/pages/view/UserMessagesView.java index 8ce129a..6299c91 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/UserMessagesView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/UserMessagesView.java @@ -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; diff --git a/src/main/java/de/assecutor/votianlt/security/SecurityService.java b/src/main/java/de/assecutor/votianlt/security/SecurityService.java index 54dc19f..f2a0766 100644 --- a/src/main/java/de/assecutor/votianlt/security/SecurityService.java +++ b/src/main/java/de/assecutor/votianlt/security/SecurityService.java @@ -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 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(); } diff --git a/src/main/java/de/assecutor/votianlt/security/SessionAuthenticationService.java b/src/main/java/de/assecutor/votianlt/security/SessionAuthenticationService.java new file mode 100644 index 0000000..598374d --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/security/SessionAuthenticationService.java @@ -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 getCurrentSessionId() { + VaadinSession vaadinSession = VaadinSession.getCurrent(); + if (vaadinSession == null || vaadinSession.getSession() == null) { + return Optional.empty(); + } + return Optional.ofNullable(vaadinSession.getSession().getId()); + } +} diff --git a/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java b/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java index 53ca97d..acca170 100644 --- a/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java +++ b/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java @@ -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("'"); diff --git a/src/main/java/de/assecutor/votianlt/service/DemoModeService.java b/src/main/java/de/assecutor/votianlt/service/DemoModeService.java new file mode 100644 index 0000000..4b70962 --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/service/DemoModeService.java @@ -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 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 demoAppUsers = appUserRepository.findByErstelltVon(demoUserId); + cleanupDemoAppUserData(demoAppUsers); + + List demoJobs = jobRepository.findByCreatedBy(demoUserIdHex); + cleanupDemoJobData(demoJobs); + + List demoCustomers = customerRepository.findByOwner(demoUserId); + if (!demoCustomers.isEmpty()) { + customerRepository.deleteAll(demoCustomers); + } + + serviceRepository.deleteByUserId(demoUserIdHex); + + List demoTaskTemplates = taskTemplateRepository.findByUserIdOrderByTemplateNameAsc(demoUserId); + if (!demoTaskTemplates.isEmpty()) { + taskTemplateRepository.deleteAll(demoTaskTemplates); + } + + invoiceTemplateRepository.deleteByUserId(demoUserIdHex); + userInvoiceDataRepository.deleteByUserId(demoUserId); + + List demoInvoices = customerInvoiceRepository.findByUserId(demoUserIdHex); + if (!demoInvoices.isEmpty()) { + customerInvoiceRepository.deleteAll(demoInvoices); + } + + if (!demoAppUsers.isEmpty()) { + appUserRepository.deleteAll(demoAppUsers); + } + } + + public void seedDemoData() { + User demoUser = getDemoUser(); + List services = seedDemoServices(demoUser); + List customers = seedDemoCustomers(demoUser); + seedDemoJobs(demoUser, customers, services); + seedDemoInvoiceTemplate(demoUser); + } + + private void cleanupDemoAppUserData(List 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 demoJobs) { + if (demoJobs.isEmpty()) { + return; + } + + Set stationIds = new LinkedHashSet<>(); + Set jobIds = new LinkedHashSet<>(); + Set 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 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 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 taskIds) { + for (ObjectId taskId : taskIds) { + if (taskId == null) { + continue; + } + + List photos = photoRepository.findByTaskId(taskId); + if (!photos.isEmpty()) { + photoRepository.deleteAll(photos); + } + + List signatures = signatureRepository.findByTaskId(taskId); + if (!signatures.isEmpty()) { + signatureRepository.deleteAll(signatures); + } + + List barcodes = barcodeRepository.findByTaskId(taskId); + if (!barcodes.isEmpty()) { + barcodeRepository.deleteAll(barcodes); + } + + List comments = commentRepository.findByTaskIdOrderByCreatedAtDesc(taskId); + if (!comments.isEmpty()) { + commentRepository.deleteAll(comments); + } + } + } + + private List dedupeInvoices(List invoices) { + Map 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 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 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 customers, List services) { + Map customersByCompany = new LinkedHashMap<>(); + for (Customer customer : customers) { + customersByCompany.put(customer.getCompanyName(), customer); + } + Map 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); + } +} diff --git a/src/main/java/de/assecutor/votianlt/service/DemoSessionRegistry.java b/src/main/java/de/assecutor/votianlt/service/DemoSessionRegistry.java new file mode 100644 index 0000000..caca79a --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/service/DemoSessionRegistry.java @@ -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 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 getActiveSessionId() { + return Optional.ofNullable(activeSessionId.get()); + } +} diff --git a/src/main/resources/messages_de.properties b/src/main/resources/messages_de.properties index 8feef7a..c89f879 100644 --- a/src/main/resources/messages_de.properties +++ b/src/main/resources/messages_de.properties @@ -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 diff --git a/src/main/resources/messages_ee.properties b/src/main/resources/messages_ee.properties index 30afab4..dde8dd1 100644 --- a/src/main/resources/messages_ee.properties +++ b/src/main/resources/messages_ee.properties @@ -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 diff --git a/src/main/resources/messages_en.properties b/src/main/resources/messages_en.properties index 30eb2e6..ec1d6bd 100644 --- a/src/main/resources/messages_en.properties +++ b/src/main/resources/messages_en.properties @@ -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 diff --git a/src/main/resources/messages_es.properties b/src/main/resources/messages_es.properties index dcca2f1..57797d5 100644 --- a/src/main/resources/messages_es.properties +++ b/src/main/resources/messages_es.properties @@ -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 diff --git a/src/main/resources/messages_fr.properties b/src/main/resources/messages_fr.properties index c1565c3..89478bc 100644 --- a/src/main/resources/messages_fr.properties +++ b/src/main/resources/messages_fr.properties @@ -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 diff --git a/src/main/resources/messages_lt.properties b/src/main/resources/messages_lt.properties index 1269621..85ed922 100644 --- a/src/main/resources/messages_lt.properties +++ b/src/main/resources/messages_lt.properties @@ -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 diff --git a/src/main/resources/messages_lv.properties b/src/main/resources/messages_lv.properties index cf4b423..4b5094d 100644 --- a/src/main/resources/messages_lv.properties +++ b/src/main/resources/messages_lv.properties @@ -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 diff --git a/src/main/resources/messages_pl.properties b/src/main/resources/messages_pl.properties index 9a17adb..615febe 100644 --- a/src/main/resources/messages_pl.properties +++ b/src/main/resources/messages_pl.properties @@ -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 diff --git a/src/main/resources/messages_ru.properties b/src/main/resources/messages_ru.properties index 893a97c..636e696 100644 --- a/src/main/resources/messages_ru.properties +++ b/src/main/resources/messages_ru.properties @@ -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=Мастер настройки diff --git a/src/main/resources/messages_tr.properties b/src/main/resources/messages_tr.properties index bc15675..07d0be5 100644 --- a/src/main/resources/messages_tr.properties +++ b/src/main/resources/messages_tr.properties @@ -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 diff --git a/src/main/resources/templates/demo_invoice_template_default.json b/src/main/resources/templates/demo_invoice_template_default.json new file mode 100644 index 0000000..abb6a7b --- /dev/null +++ b/src/main/resources/templates/demo_invoice_template_default.json @@ -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 + } + ] +} diff --git a/src/test/java/de/assecutor/votianlt/service/DemoModeServiceTest.java b/src/test/java/de/assecutor/votianlt/service/DemoModeServiceTest.java new file mode 100644 index 0000000..1d5aeda --- /dev/null +++ b/src/test/java/de/assecutor/votianlt/service/DemoModeServiceTest.java @@ -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> jobsCaptor; + @Captor + private ArgumentCaptor> servicesCaptor; + @Captor + private ArgumentCaptor 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(); + } +} diff --git a/src/test/java/de/assecutor/votianlt/service/DemoSessionRegistryTest.java b/src/test/java/de/assecutor/votianlt/service/DemoSessionRegistryTest.java new file mode 100644 index 0000000..22d8a7e --- /dev/null +++ b/src/test/java/de/assecutor/votianlt/service/DemoSessionRegistryTest.java @@ -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(); + } +}