From 4f3f75c8a80600ffd8b30f37c32ad403e3ab8531 Mon Sep 17 00:00:00 2001 From: Sven Carstensen Date: Mon, 1 Sep 2025 13:44:41 +0200 Subject: [PATCH] Erweiterungen --- src/main/frontend/styles/pdf-builder.css | 281 ------ .../controller/MessageController.java | 73 +- .../assecutor/votianlt/model/PdfTemplate.java | 56 -- .../pages/service/PDFGenerationService.java | 42 - .../pages/service/PdfTemplateService.java | 32 - .../votianlt/pages/view/AddJobView.java | 42 +- .../votianlt/pages/view/PDFBuilderView.java | 870 ------------------ .../votianlt/repository/JobRepository.java | 5 + .../repository/PdfTemplateRepository.java | 10 - 9 files changed, 112 insertions(+), 1299 deletions(-) delete mode 100644 src/main/frontend/styles/pdf-builder.css delete mode 100644 src/main/java/de/assecutor/votianlt/model/PdfTemplate.java delete mode 100644 src/main/java/de/assecutor/votianlt/pages/service/PDFGenerationService.java delete mode 100644 src/main/java/de/assecutor/votianlt/pages/service/PdfTemplateService.java delete mode 100644 src/main/java/de/assecutor/votianlt/pages/view/PDFBuilderView.java delete mode 100644 src/main/java/de/assecutor/votianlt/repository/PdfTemplateRepository.java diff --git a/src/main/frontend/styles/pdf-builder.css b/src/main/frontend/styles/pdf-builder.css deleted file mode 100644 index 808536b..0000000 --- a/src/main/frontend/styles/pdf-builder.css +++ /dev/null @@ -1,281 +0,0 @@ -/* Styles for PDF Builder View */ - -.pdf-builder-root { - display: flex; - flex-direction: column; - gap: var(--lumo-space-m); - width: 90%; - max-width: 90%; - margin: 10px auto 0 auto; - padding: 20px; -} - -.pdf-builder-workspace { - display: flex; - gap: var(--lumo-space-l); - align-items: flex-start; - width: 100%; - overflow: hidden; -} - -.pdf-palette { - flex: 0 1 15%; - min-width: 10%; - max-width: 20%; - border: 1px solid var(--lumo-contrast-20pct); - border-radius: var(--lumo-border-radius-m); - background: var(--lumo-base-color); - box-shadow: var(--lumo-box-shadow-xs); - padding: var(--lumo-space-m); -} - -.pdf-palette .palette-title { - font-weight: 600; - margin-bottom: var(--lumo-space-s); -} - -.pdf-palette .tool-item { - display: flex; - align-items: center; - gap: var(--lumo-space-m); - padding: var(--lumo-space-s); - border: 1px dashed var(--lumo-contrast-20pct); - border-radius: var(--lumo-border-radius-s); - cursor: grab; - user-select: none; -} - -.pdf-palette .tool-item + .tool-item { margin-top: var(--lumo-space-s); } - -.pdf-palette .tool-item:hover { - background: var(--lumo-contrast-5pct); -} - -.pdf-canvas-wrapper { - flex: 1 1 auto; - display: flex; - justify-content: center; - min-width: 0; - overflow: hidden; -} - -/* Inspector (right side) */ -.pdf-inspector { - flex: 0 1 15%; - min-width: 10%; - max-width: 20%; - border: 1px solid var(--lumo-contrast-20pct); - border-radius: var(--lumo-border-radius-m); - background: var(--lumo-base-color); - box-shadow: var(--lumo-box-shadow-xs); - padding: var(--lumo-space-m); - max-height: calc(100vh - 220px); - overflow: auto; -} - -.pdf-inspector .inspector-title { - font-weight: 600; - margin-bottom: var(--lumo-space-s); -} - -.inspector-item { - display: flex; - flex-direction: column; - gap: var(--lumo-space-xs); - padding: var(--lumo-space-s) 0; - border-bottom: 1px dashed var(--lumo-contrast-10pct); - margin-bottom: var(--lumo-space-s); -} - -.inspector-item:last-child { - border-bottom: none; - margin-bottom: 0; -} - -.inspector-item .item-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: var(--lumo-space-xs); -} - -.inspector-item .item-label { - font-weight: 500; - font-size: var(--lumo-font-size-s); - color: var(--lumo-body-text-color); - margin: 0; -} - -.inspector-item .field-collapse-button { - background: none; - border: none; - cursor: pointer; - padding: var(--lumo-space-xs); - border-radius: var(--lumo-border-radius-s); - color: var(--lumo-secondary-text-color); - display: flex; - align-items: center; - justify-content: center; - min-width: 20px; - height: 20px; -} - -.inspector-item .field-collapse-button:hover { - background: var(--lumo-contrast-10pct); - color: var(--lumo-body-text-color); -} - -.inspector-item .item-fields { - display: flex; - flex-direction: column; - gap: var(--lumo-space-xs); - transition: opacity 0.3s ease, max-height 0.3s ease; - max-height: 1000px; - overflow: hidden; -} - -.inspector-item.fields-collapsed .item-fields { - opacity: 0; - max-height: 0; - margin: 0; - padding: 0; -} - -.pdf-inspector vaadin-number-field { - width: 100%; -} - -.pdf-canvas { - width: 100%; - aspect-ratio: 210 / 297; /* A4 */ - position: relative; - background: white; - border: 1px solid var(--lumo-contrast-20pct); - box-shadow: var(--lumo-box-shadow-m); - background-image: linear-gradient(90deg, rgba(0,0,0,0.03) 1px, transparent 1px), - linear-gradient(rgba(0,0,0,0.03) 1px, transparent 1px); - background-size: 20px 20px; -} - -.canvas-frame { - position: absolute; - border: 3px solid var(--lumo-contrast-30pct); /* inactive: light gray */ - border-radius: var(--lumo-border-radius-s); - background: rgba(255,255,255,0.9); - resize: none; - overflow: auto; - min-width: 5%; - min-height: 5%; - cursor: default; - user-select: none; -} - -.canvas-frame.active { - border: 3px solid var(--lumo-primary-color); /* active: blue */ - box-shadow: none; - resize: both; - cursor: default; -} - -.canvas-frame .text-editor { - width: 100%; - height: 100%; - cursor: text; - user-select: text; - white-space: pre-wrap; - word-break: break-word; - overflow-wrap: anywhere; -} - -.canvas-frame .text-toolbar { - position: absolute; - left: 0; - top: -40px; - display: none; - align-items: center; - gap: var(--lumo-space-xs); - background: var(--lumo-base-color); - border: 1px solid var(--lumo-contrast-20pct); - border-radius: var(--lumo-border-radius-s); - box-shadow: var(--lumo-box-shadow-xs); - padding: 2px 4px; - cursor: default; - user-select: none; -} - -.canvas-frame.active .text-toolbar { display: flex; } - -.canvas-frame img { width: 100%; height: 100%; object-fit: contain; display: block; } - -.pdf-builder-actions { display: flex; justify-content: flex-end; } - -/* Capturing mode: hide frame borders, handles, toolbars for save/export */ -.pdf-canvas.capturing .canvas-frame, -.pdf-canvas.capturing .canvas-frame.active { - border-color: transparent !important; /* hide border but keep layout */ - box-shadow: none !important; - resize: none !important; -} -.pdf-canvas.capturing .frame-handle-move, -.pdf-canvas.capturing .text-toolbar { - display: none !important; -} - -/* Export-only: make box backgrounds transparent */ -.pdf-canvas.capturing-export .canvas-frame, -.pdf-canvas.capturing-export .text-editor { - background: transparent !important; -} - -/* Image frames: no scrollbars */ -.image-frame { - overflow: hidden; /* hide scrollbars */ - -ms-overflow-style: none; /* IE/Edge */ - scrollbar-width: none; /* Firefox */ -} -.image-frame::-webkit-scrollbar { display: none; } /* WebKit */ - -/* Text frames: no scrolling allowed */ -.text-frame { - overflow: hidden; /* disable scrolling */ - -ms-overflow-style: none; /* IE/Edge */ - scrollbar-width: none; /* Firefox */ -} -.text-frame::-webkit-scrollbar { display: none; } /* WebKit */ - -/* Also disable scrolling inside the text editor */ -.text-frame .text-editor { - overflow: hidden; /* disable scrolling */ - -ms-overflow-style: none; /* IE/Edge */ - scrollbar-width: none; /* Firefox */ -} -.text-frame .text-editor::-webkit-scrollbar { display: none; } /* WebKit */ - -/* Move handle (top-left) visible only when active */ -.frame-handle-move { - position: absolute; - top: 0; - left: 0; - width: 16px; - height: 16px; - display: none; - align-items: center; - justify-content: center; - font-size: 10px; - line-height: 1; - color: var(--lumo-primary-color); - background: #fff; - border: 1px solid var(--lumo-primary-color); - border-radius: 4px; - box-shadow: var(--lumo-box-shadow-xs); - cursor: grab; - z-index: 2; - user-select: none; - transform: translate(-50%, -50%); /* center the handle on the top-left corner */ -} - -.canvas-frame.active .frame-handle-move { display: flex; } - -.frame-handle-move:active { cursor: grabbing; } - -/* bottom-right visual icon removed; native CSS resize handle remains */ diff --git a/src/main/java/de/assecutor/votianlt/controller/MessageController.java b/src/main/java/de/assecutor/votianlt/controller/MessageController.java index 6246fd6..050add6 100644 --- a/src/main/java/de/assecutor/votianlt/controller/MessageController.java +++ b/src/main/java/de/assecutor/votianlt/controller/MessageController.java @@ -3,8 +3,11 @@ package de.assecutor.votianlt.controller; import de.assecutor.votianlt.dto.AppLoginRequest; import de.assecutor.votianlt.dto.AppLoginResponse; import de.assecutor.votianlt.model.AppUser; +import de.assecutor.votianlt.model.Job; import de.assecutor.votianlt.pages.service.AppUserService; import de.assecutor.votianlt.repository.AppUserRepository; +import de.assecutor.votianlt.repository.JobRepository; +import lombok.extern.slf4j.Slf4j; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.messaging.simp.SimpMessagingTemplate; @@ -14,6 +17,7 @@ import org.springframework.beans.factory.annotation.Autowired; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.List; import java.util.Map; /** @@ -21,6 +25,7 @@ import java.util.Map; * Provides endpoints for sending and receiving messages via WebSocket/STOMP. */ @Controller +@Slf4j public class MessageController { @Autowired @@ -32,16 +37,22 @@ public class MessageController { @Autowired private AppUserService appUserService; + @Autowired + private JobRepository jobRepository; + /** * Handles messages sent to /app/message and broadcasts them to all subscribers of /topic/messages */ @MessageMapping("/message") @SendTo("/topic/messages") public Map handleMessage(Map message) { + log.info("STOMP Endpoint '/app/message' called with data: {}", message); + // Add timestamp to the message message.put("timestamp", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); message.put("processed", true); + log.info("STOMP Response for '/app/message' sent to '/topic/messages': {}", message); return message; } @@ -51,9 +62,12 @@ public class MessageController { @MessageMapping("/job/status") @SendTo("/topic/job-updates") public Map handleJobStatusUpdate(Map jobUpdate) { + log.info("STOMP Endpoint '/app/job/status' called with data: {}", jobUpdate); + jobUpdate.put("timestamp", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); jobUpdate.put("source", "app"); + log.info("STOMP Response for '/app/job/status' sent to '/topic/job-updates': {}", jobUpdate); return jobUpdate; } @@ -63,9 +77,12 @@ public class MessageController { @MessageMapping("/device/location") @SendTo("/topic/device-locations") public Map handleDeviceLocation(Map locationUpdate) { + log.info("STOMP Endpoint '/app/device/location' called with data: {}", locationUpdate); + locationUpdate.put("timestamp", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); locationUpdate.put("processed", true); + log.info("STOMP Response for '/app/device/location' sent to '/topic/device-locations': {}", locationUpdate); return locationUpdate; } @@ -79,7 +96,9 @@ public class MessageController { "type", "notification" ); + log.info("Sending notification to user '{}': {}", username, notification); messagingTemplate.convertAndSendToUser(username, "/queue/notifications", notification); + log.info("Notification sent to '/user/{}/queue/notifications'", username); } /** @@ -92,7 +111,9 @@ public class MessageController { "type", "broadcast" ); + log.info("Sending broadcast message: {}", broadcast); messagingTemplate.convertAndSend("/topic/broadcasts", broadcast); + log.info("Broadcast message sent to '/topic/broadcasts'"); } /** @@ -103,21 +124,65 @@ public class MessageController { @MessageMapping("/auth/login") @SendToUser("/queue/auth") public AppLoginResponse handleAppLogin(AppLoginRequest request) { + log.info("STOMP Endpoint '/app/auth/login' called with email: {}", + request != null ? request.getEmail() : "null"); + if (request == null || request.getEmail() == null || request.getPassword() == null || request.getEmail().isBlank() || request.getPassword().isBlank()) { - return new AppLoginResponse(false, "E-Mail und Passwort sind erforderlich", null); + AppLoginResponse response = new AppLoginResponse(false, "E-Mail und Passwort sind erforderlich", null); + log.info("STOMP Response for '/app/auth/login' sent to '/user/queue/auth': success={}, message='{}'", + false, "E-Mail und Passwort sind erforderlich"); + return response; } AppUser user = appUserRepository.findByEmail(request.getEmail()); if (user == null) { - return new AppLoginResponse(false, "Benutzer nicht gefunden", null); + AppLoginResponse response = new AppLoginResponse(false, "Benutzer nicht gefunden", null); + log.info("STOMP Response for '/app/auth/login' sent to '/user/queue/auth': success={}, message='{}'", + false, "Benutzer nicht gefunden"); + return response; } boolean ok = appUserService.verifyPassword(request.getPassword(), user.getPassword()); if (!ok) { - return new AppLoginResponse(false, "Ungültige Anmeldedaten", null); + AppLoginResponse response = new AppLoginResponse(false, "Ungültige Anmeldedaten", null); + log.info("STOMP Response for '/app/auth/login' sent to '/user/queue/auth': success={}, message='{}'", + false, "Ungültige Anmeldedaten"); + return response; } - return new AppLoginResponse(true, "Anmeldung erfolgreich", user.getId() != null ? user.getId().toHexString() : null); + AppLoginResponse response = new AppLoginResponse(true, "Anmeldung erfolgreich", user.getId() != null ? user.getId().toHexString() : null); + log.info("STOMP Response for '/app/auth/login' sent to '/user/queue/auth': success={}, message='{}', appUserId='{}'", + true, "Anmeldung erfolgreich", response.getAppUserId()); + return response; + } + + /** + * Endpoint to retrieve jobs assigned to a specific app user. + * Client sends to /app/jobs/assigned with payload { appUserId }. + * The response is sent back to the requesting user on /user/queue/jobs + */ + @MessageMapping("/jobs/assigned") + @SendToUser("/queue/jobs") + public List handleGetAssignedJobs(Map request) { + log.info("STOMP Endpoint '/app/jobs/assigned' called with data: {}", request); + + if (request == null || !request.containsKey("appUserId")) { + log.info("STOMP Response for '/app/jobs/assigned' sent to '/user/queue/jobs': empty list (no appUserId provided)"); + return List.of(); // Return empty list if no appUserId provided + } + + String appUserId = request.get("appUserId").toString(); + if (appUserId == null || appUserId.isBlank()) { + log.info("STOMP Response for '/app/jobs/assigned' sent to '/user/queue/jobs': empty list (appUserId is blank)"); + return List.of(); // Return empty list if appUserId is blank + } + + // Find jobs assigned to this app user + List assignedJobs = jobRepository.findByAppUser(appUserId); + log.info("STOMP Response for '/app/jobs/assigned' sent to '/user/queue/jobs': {} jobs found for appUserId='{}'", + assignedJobs.size(), appUserId); + + return assignedJobs; } } \ No newline at end of file diff --git a/src/main/java/de/assecutor/votianlt/model/PdfTemplate.java b/src/main/java/de/assecutor/votianlt/model/PdfTemplate.java deleted file mode 100644 index 8a66b8a..0000000 --- a/src/main/java/de/assecutor/votianlt/model/PdfTemplate.java +++ /dev/null @@ -1,56 +0,0 @@ -package de.assecutor.votianlt.model; - -import org.springframework.data.annotation.Id; -import org.springframework.data.mongodb.core.mapping.Document; - -import java.time.Instant; - -@Document(collection = "pdf_templates") -public class PdfTemplate { - @Id - private String id; - private String name; - private String dataJson; // raw JSON of canvas state - private Instant createdAt; - private Instant updatedAt; - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getDataJson() { - return dataJson; - } - - public void setDataJson(String dataJson) { - this.dataJson = dataJson; - } - - public Instant getCreatedAt() { - return createdAt; - } - - public void setCreatedAt(Instant createdAt) { - this.createdAt = createdAt; - } - - public Instant getUpdatedAt() { - return updatedAt; - } - - public void setUpdatedAt(Instant updatedAt) { - this.updatedAt = updatedAt; - } -} diff --git a/src/main/java/de/assecutor/votianlt/pages/service/PDFGenerationService.java b/src/main/java/de/assecutor/votianlt/pages/service/PDFGenerationService.java deleted file mode 100644 index f981ac8..0000000 --- a/src/main/java/de/assecutor/votianlt/pages/service/PDFGenerationService.java +++ /dev/null @@ -1,42 +0,0 @@ -package de.assecutor.votianlt.pages.service; - -import com.lowagie.text.Document; -import com.lowagie.text.DocumentException; -import com.lowagie.text.Image; -import com.lowagie.text.PageSize; -import com.lowagie.text.pdf.PdfWriter; -import org.springframework.stereotype.Service; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; - -@Service -public class PDFGenerationService { - - public byte[] generatePdfFromImage(byte[] pngBytes) { - try { - Document document = new Document(PageSize.A4); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - PdfWriter.getInstance(document, baos); - document.open(); - - Image image = Image.getInstance(pngBytes); - // Scale image to fit A4 with small margins - float margin = 20f; - float maxW = PageSize.A4.getWidth() - 2 * margin; - float maxH = PageSize.A4.getHeight() - 2 * margin; - image.scaleToFit(maxW, maxH); - // Center the image - float x = (PageSize.A4.getWidth() - image.getScaledWidth()) / 2f; - float y = (PageSize.A4.getHeight() - image.getScaledHeight()) / 2f; - image.setAbsolutePosition(x, y); - document.add(image); - - document.close(); - return baos.toByteArray(); - } catch (DocumentException | IOException e) { - throw new RuntimeException("Fehler bei der PDF-Erzeugung", e); - } - } -} diff --git a/src/main/java/de/assecutor/votianlt/pages/service/PdfTemplateService.java b/src/main/java/de/assecutor/votianlt/pages/service/PdfTemplateService.java deleted file mode 100644 index af886eb..0000000 --- a/src/main/java/de/assecutor/votianlt/pages/service/PdfTemplateService.java +++ /dev/null @@ -1,32 +0,0 @@ -package de.assecutor.votianlt.pages.service; - -import de.assecutor.votianlt.model.PdfTemplate; -import de.assecutor.votianlt.repository.PdfTemplateRepository; -import org.springframework.stereotype.Service; - -import java.time.Instant; -import java.util.Optional; - -@Service -public class PdfTemplateService { - private final PdfTemplateRepository repository; - - public PdfTemplateService(PdfTemplateRepository repository) { - this.repository = repository; - } - - public PdfTemplate saveOrUpdate(String name, String dataJson) { - Optional existing = repository.findByName(name); - PdfTemplate t = existing.orElseGet(PdfTemplate::new); - t.setName(name); - t.setDataJson(dataJson); - Instant now = Instant.now(); - if (t.getCreatedAt() == null) t.setCreatedAt(now); - t.setUpdatedAt(now); - return repository.save(t); - } - - public Optional findByName(String name) { - return repository.findByName(name); - } -} diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java b/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java index e98d785..d96be2a 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java @@ -34,6 +34,8 @@ import de.assecutor.votianlt.pages.service.CustomerService; import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar; import de.assecutor.votianlt.pages.service.AddCustomerService; import de.assecutor.votianlt.model.Customer; +import de.assecutor.votianlt.pages.service.AppUserService; +import de.assecutor.votianlt.model.AppUser; import jakarta.annotation.security.RolesAllowed; import lombok.extern.slf4j.Slf4j; @@ -52,6 +54,7 @@ public class AddJobView extends Main { private final AddJobService addJobService; private final CustomerService customerService; private final AddCustomerService addCustomerService; + private final AppUserService appUserService; // Customer selection private ComboBox customerSelection; @@ -85,7 +88,7 @@ public class AddJobView extends Main { // Digital processing private Checkbox digitalProcessing; - private ComboBox appUser; + private ComboBox appUser; // Price field private TextField price; @@ -121,11 +124,15 @@ public class AddJobView extends Main { // Mapping für die Anzeige-Labels der Kunden zur Entität private final Map customerLabelToEntity = new LinkedHashMap<>(); + + // Available app users for the current user + private List availableAppUsers; - public AddJobView(AddJobService addJobService, AddCustomerService addCustomerService, CustomerService customerService) { + public AddJobView(AddJobService addJobService, AddCustomerService addCustomerService, CustomerService customerService, AppUserService appUserService) { this.addJobService = addJobService; this.addCustomerService = addCustomerService; this.customerService = customerService; + this.appUserService = appUserService; initializeComponents(); setupLayout(); setupValidation(); @@ -267,7 +274,11 @@ public class AddJobView extends Main { // Digital processing digitalProcessing = new Checkbox("Digitale Abwicklung per App"); appUser = new ComboBox<>("App-Nutzer"); - appUser.setItems("App-Nutzer"); + + // Load app users for current user and set up the ComboBox + availableAppUsers = appUserService.findByCurrentUser(); + appUser.setItems(availableAppUsers); + appUser.setItemLabelGenerator(user -> user.getVorname() + " " + user.getNachname() + " (" + user.getEmail() + ")"); appUser.setPlaceholder("App-Nutzer auswählen..."); // Price field @@ -762,7 +773,22 @@ public class AddJobView extends Main { binder.bind(deliveryAddressAddition, Job::getDeliveryAddressAddition, Job::setDeliveryAddressAddition); binder.bind(digitalProcessing, Job::isDigitalProcessing, Job::setDigitalProcessing); - binder.bind(appUser, Job::getAppUser, Job::setAppUser); + + // Bind appUser with converter: AppUser object <-> String ID + binder.forField(appUser) + .withConverter( + // Convert AppUser to String (ID) + user -> user != null ? user.getId().toHexString() : null, + // Convert String (ID) back to AppUser + id -> { + if (id == null || id.trim().isEmpty()) return null; + return availableAppUsers.stream() + .filter(user -> user.getId().toHexString().equals(id)) + .findFirst() + .orElse(null); + } + ) + .bind(Job::getAppUser, Job::setAppUser); // Set up validation triggers and visual styling setupValidationTriggers(); @@ -938,6 +964,14 @@ public class AddJobView extends Main { // Validate all required fields using the binder if (binder.writeBeanIfValid(job)) { + // Additional validation: If digital processing is enabled, app user must be selected + if (digitalProcessing.getValue() && appUser.getValue() == null) { + Notification errorNotification = Notification.show( + "Wenn digitale Abwicklung per App aktiviert ist, muss ein App-Nutzer ausgewählt werden."); + errorNotification.setDuration(5000); + return; + } + // Ensure at least one cargo item is provided (tasks may be empty) // Definition: Ein Cargo-Item gilt nur als gefüllt, wenn eine Beschreibung vorhanden ist List cargoFilled = cargoItemsState.stream() diff --git a/src/main/java/de/assecutor/votianlt/pages/view/PDFBuilderView.java b/src/main/java/de/assecutor/votianlt/pages/view/PDFBuilderView.java deleted file mode 100644 index 7c99cb1..0000000 --- a/src/main/java/de/assecutor/votianlt/pages/view/PDFBuilderView.java +++ /dev/null @@ -1,870 +0,0 @@ -package de.assecutor.votianlt.pages.view; - -import com.vaadin.flow.component.UI; -import com.vaadin.flow.component.button.Button; -import com.vaadin.flow.component.button.ButtonVariant; -import com.vaadin.flow.component.dependency.CssImport; -import com.vaadin.flow.component.dialog.Dialog; -import com.vaadin.flow.component.html.Div; -import com.vaadin.flow.component.html.H3; -import com.vaadin.flow.component.html.Image; -import com.vaadin.flow.component.html.Span; -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.orderedlayout.HorizontalLayout; -import com.vaadin.flow.component.orderedlayout.VerticalLayout; -import com.vaadin.flow.component.page.PendingJavaScriptResult; -import com.vaadin.flow.component.upload.Upload; -import com.vaadin.flow.component.upload.receivers.MemoryBuffer; -import com.vaadin.flow.router.PageTitle; -import com.vaadin.flow.router.Route; -import com.vaadin.flow.router.Menu; -import com.vaadin.flow.server.StreamResource; -import com.vaadin.flow.server.StreamRegistration; -import com.vaadin.flow.server.VaadinSession; - -import de.assecutor.votianlt.pages.base.ui.view.MainLayout; -import de.assecutor.votianlt.pages.service.PDFGenerationService; -import de.assecutor.votianlt.pages.service.PdfTemplateService; - -import jakarta.annotation.security.RolesAllowed; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.Serial; -import java.nio.charset.StandardCharsets; -import java.util.Base64; -import java.util.Objects; -import com.vaadin.flow.component.textfield.TextField; -import com.vaadin.flow.component.textfield.NumberField; -import com.vaadin.flow.component.KeyPressEvent; -import com.vaadin.flow.component.checkbox.Checkbox; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.util.List; -import java.util.ArrayList; -import java.util.Set; -import java.util.HashSet; - -@Route(value = "pdf-builder", layout = MainLayout.class) -@PageTitle("PDF Builder") -@Menu(order = 6, icon = "vaadin:file-text", title = "PDF Builder") -@RolesAllowed("USER") -@CssImport("./styles/pdf-builder.css") -public class PDFBuilderView extends Div { - - private final PDFGenerationService pdfService; - private final PdfTemplateService templateService; - private final ObjectMapper objectMapper = new ObjectMapper(); - private VerticalLayout inspectorList; - // Serverseitige Fallback-Liste der Frames (zeigt Einträge selbst dann an, - // wenn das DOM-Lesen via JS temporär fehlschlägt) - private final List serverFrames = new ArrayList<>(); - // Track which items are collapsed by their ID - private final Set collapsedItems = new HashSet<>(); - // Temporarily store element name from dialog - private String currentElementName; - // Temporarily store content inclusion preference from dialog - private boolean currentIncludeContent; - - public PDFBuilderView(PDFGenerationService pdfService, PdfTemplateService templateService) { - this.pdfService = pdfService; - this.templateService = templateService; - addClassName("pdf-builder-root"); - setSizeFull(); - - // Workspace (Palette links, Canvas mittig) - Div workspace = new Div(); - workspace.addClassName("pdf-builder-workspace"); - - Div palette = createPalette(); - PdfCanvas canvas = new PdfCanvas(); - Div inspector = createInspector(); - - workspace.add(palette, canvas, inspector); - add(workspace); - - // Aktionen unten: Speichern - HorizontalLayout actions = new HorizontalLayout(); - actions.addClassName("pdf-builder-actions"); - actions.setWidthFull(); - Button save = new Button("Speichern", e -> openSaveDialog()); - save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - actions.add(save); - add(actions); - - // initialer Refresh (leer zu Beginn) - refreshInspectorFromDom(); - } - - private void openSaveDialog() { - Dialog dlg = new Dialog(); - dlg.setHeaderTitle("Template speichern"); - TextField name = new TextField("Template-Name"); - name.setWidthFull(); - name.setRequired(true); - VerticalLayout content = new VerticalLayout(name); - content.setPadding(false); - content.setSpacing(true); - dlg.add(content); - Button cancel = new Button("Abbrechen", e -> dlg.close()); - Button save = new Button("Speichern & PDF", e -> { - if (name.isEmpty()) { - Notification.show("Bitte einen Namen angeben", 3000, Notification.Position.MIDDLE); - return; - } - collectAndSave(name.getValue()); - dlg.close(); - }); - save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - dlg.getFooter().add(cancel, save); - dlg.open(); - } - - private void collectAndSave(String templateName) { - String js = getCollectStateJs(); - PendingJavaScriptResult res = UI.getCurrent().getPage().executeJs(js); - res.then(String.class, json -> { - try { - templateService.saveOrUpdate(templateName, json); - Notification.show("Template gespeichert", 2000, Notification.Position.BOTTOM_END); - } catch (Exception ex) { - Notification.show("Speichern fehlgeschlagen: " + ex.getMessage(), 4000, Notification.Position.MIDDLE); - } - // Danach PDF exportieren (Dateiname aus Template-Namen) - exportCanvasToPdf(templateName); - }); - } - - private String getCollectStateJs() { - return "return (function(){" + - " const root = document.getElementById('pdf-canvas');\n" + - " if(!root){ return JSON.stringify({items:[]}); }\n" + - " root.classList.add('capturing');\n" + - " try {\n" + - " const rect = root.getBoundingClientRect();\n" + - " const items = Array.from(root.querySelectorAll('.canvas-frame')).map((f, idx)=>{\n" + - " const csf = getComputedStyle(f);\n" + - " const x = Math.round(parseFloat(csf.left) || 0);\n" + - " const y = Math.round(parseFloat(csf.top) || 0);\n" + - " const width = Math.round(parseFloat(csf.width) || f.getBoundingClientRect().width);\n" + - " const height = Math.round(parseFloat(csf.height) || f.getBoundingClientRect().height);\n" + - " const z = (function(){ const zc = parseInt(csf.zIndex); return isNaN(zc)? idx : zc; })();\n" + - " const name = f.getAttribute('data-name') || '';\n" + - " const includeContent = f.getAttribute('data-include-content') !== 'false';\n" + - " const img = f.querySelector('img');\n" + - " if(img){\n" + - " return {type:'image', name, x, y, width, height, z, dataUrl: includeContent ? img.src : ''};\n" + - " } else {\n" + - " const editor = f.querySelector('.text-editor');\n" + - " const cs = editor ? getComputedStyle(editor) : null;\n" + - " return {type:'text', name, x, y, width, height, z, html: includeContent ? (editor?editor.innerHTML:'') : '', editorStyle: includeContent ? (cs?{fontSize: cs.fontSize, color: cs.color, lineHeight: cs.lineHeight}:{}) : {}};\n" + - " }\n" + - " });\n" + - " return JSON.stringify({canvas:{width: Math.round(rect.width), height: Math.round(rect.height)}, items});\n" + - " } finally {\n" + - " root.classList.remove('capturing');\n" + - " }\n" + - "})()"; - } - - private Div createInspector() { - Div inspector = new Div(); - inspector.addClassName("pdf-inspector"); - - H3 title = new H3("Elemente"); - title.addClassName("inspector-title"); - - inspectorList = new VerticalLayout(); - inspectorList.setPadding(false); - inspectorList.setSpacing(false); - - inspector.add(title, inspectorList); - - return inspector; - } - - - private void toggleItemFields(Div inspectorItem, Button fieldCollapseButton) { - // Get the item ID from the element's ID (remove prefix) - String fullId = inspectorItem.getId().orElse(""); - String itemId = fullId.startsWith("inspector-item-") ? fullId.substring(15) : fullId; - - boolean isCollapsed = collapsedItems.contains(itemId); - - if (isCollapsed) { - // Expand fields - collapsedItems.remove(itemId); - inspectorItem.removeClassName("fields-collapsed"); - fieldCollapseButton.setIcon(new Icon(VaadinIcon.ANGLE_DOWN)); - } else { - // Collapse fields - collapsedItems.add(itemId); - inspectorItem.addClassName("fields-collapsed"); - fieldCollapseButton.setIcon(new Icon(VaadinIcon.ANGLE_RIGHT)); - } - } - - private void showElementNameDialog(String elementType, Runnable onNameProvided) { - Dialog dlg = new Dialog(); - dlg.setHeaderTitle("Element benennen"); - dlg.setModal(true); - - TextField nameField = new TextField("Element-Name"); - nameField.setWidthFull(); - nameField.setRequired(true); - nameField.setPlaceholder(elementType + " eingeben..."); - nameField.focus(); - - Checkbox includeContentCheckbox = new Checkbox("Inhalt in Template übernehmen"); - includeContentCheckbox.setValue(true); // Default: content included - includeContentCheckbox.getStyle().set("margin-top", "var(--lumo-space-s)"); - - VerticalLayout content = new VerticalLayout(); - content.setPadding(false); - content.setSpacing(true); - content.add( - new Span("Bitte geben Sie einen Namen für das " + elementType + "-Element ein:"), - nameField, - includeContentCheckbox - ); - dlg.add(content); - - Button cancel = new Button("Abbrechen", e -> dlg.close()); - Button confirm = new Button("Erstellen", e -> { - String name = nameField.getValue(); - if (name == null || name.trim().isEmpty()) { - Notification.show("Bitte einen Namen eingeben", 3000, Notification.Position.MIDDLE); - nameField.focus(); - return; - } - // Store the name and content inclusion preference temporarily for use in element creation - currentElementName = name.trim(); - currentIncludeContent = includeContentCheckbox.getValue(); - dlg.close(); - onNameProvided.run(); - }); - confirm.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - - // Enter key should trigger confirm - nameField.addKeyPressListener(e -> { - if (e.getKey().equals("Enter")) { - confirm.click(); - } - }); - - dlg.getFooter().add(cancel, confirm); - dlg.open(); - } - - private void refreshInspectorFromDom() { - String js = getFramesInspectorJs(); - UI.getCurrent().getPage().executeJs(js).then(String.class, json -> { - try { - String safeJson = (json == null || json.isBlank()) ? "[]" : json; - safeJson = safeJson.trim(); - if ("null".equalsIgnoreCase(safeJson) || "undefined".equalsIgnoreCase(safeJson)) { - safeJson = "[]"; - } - List frames = objectMapper.readValue(safeJson, new TypeReference>(){}); - // Erfolg: serverseitige Liste ersetzen und rendern - serverFrames.clear(); - if (frames != null) { serverFrames.addAll(frames); } - renderInspectorItems(serverFrames); - } catch (Exception ex) { - // Fallback: Letzten bekannten Stand aus serverFrames rendern - renderInspectorItems(serverFrames); - Notification.show("Inspector-Update fehlgeschlagen (Fallback genutzt): " + ex.getMessage(), 3000, Notification.Position.MIDDLE); - } - }); - } - - private void refreshInspectorFromDomDeferred() { - UI ui = UI.getCurrent(); - if (ui == null) { - refreshInspectorFromDom(); - return; - } - // Nach dem nächsten Paint (requestAnimationFrame) den Server anstoßen, - // damit der Inspector NACH den DOM-Updates liest. - ui.getPage().executeJs("requestAnimationFrame(() => { try { if ($0 && $0.$server && $0.$server.onAfterFrameMutation) { $0.$server.onAfterFrameMutation(); } } catch(e){} });", this.getElement()); - } - - @com.vaadin.flow.component.ClientCallable - private void onAfterFrameMutation() { - refreshInspectorFromDom(); - } - - private void renderInspectorItems(List frames) { - inspectorList.removeAll(); - if (frames == null || frames.isEmpty()) return; - for (FrameInfo f : frames) { - if (f == null) { continue; } - Div row = new Div(); - row.addClassName("inspector-item"); - - // Set unique ID for this inspector item to track collapse state - String itemId = f.id != null ? f.id : ""; - row.setId("inspector-item-" + itemId); - - // Check if this item should be collapsed - if not tracked yet, make it initially collapsed - if (!collapsedItems.contains(itemId)) { - collapsedItems.add(itemId); // Make new tools initially collapsed - } - boolean isItemCollapsed = collapsedItems.contains(itemId); - if (isItemCollapsed) { - row.addClassName("fields-collapsed"); - } - - // Create header for each tool with label and collapse button - Div itemHeader = new Div(); - itemHeader.addClassName("item-header"); - - // Item label - use custom name if available, otherwise fallback to type - String displayName = (f.name != null && !f.name.trim().isEmpty()) ? f.name : - ("text".equals(f.type) ? "Text" : "Bild"); - Span label = new Span(displayName); - label.addClassName("item-label"); - - // Individual collapse button for this tool's fields - Icon buttonIcon = isItemCollapsed ? new Icon(VaadinIcon.ANGLE_RIGHT) : new Icon(VaadinIcon.ANGLE_DOWN); - Button fieldCollapseButton = new Button(buttonIcon); - fieldCollapseButton.addClassName("field-collapse-button"); - fieldCollapseButton.addClickListener(e -> toggleItemFields(row, fieldCollapseButton)); - - itemHeader.add(label, fieldCollapseButton); - - // Item fields container for vertical stacking - Div fieldsContainer = new Div(); - fieldsContainer.addClassName("item-fields"); - - NumberField x = makeField("X", f.x); - NumberField y = makeField("Y", f.y); - NumberField w = makeField("Breite", f.width); - NumberField h = makeField("Höhe", f.height); - - // Update handlers (nur Client-Änderungen) - without inspector refresh to prevent auto-collapse - x.addValueChangeListener(e -> { if (e.isFromClient()) setFrameRect(f.id, x.getValue(), y.getValue(), w.getValue(), h.getValue(), false); }); - y.addValueChangeListener(e -> { if (e.isFromClient()) setFrameRect(f.id, x.getValue(), y.getValue(), w.getValue(), h.getValue(), false); }); - w.addValueChangeListener(e -> { if (e.isFromClient()) setFrameRect(f.id, x.getValue(), y.getValue(), w.getValue(), h.getValue(), false); }); - h.addValueChangeListener(e -> { if (e.isFromClient()) setFrameRect(f.id, x.getValue(), y.getValue(), w.getValue(), h.getValue(), false); }); - - // Add fields to container vertically - fieldsContainer.add(x, y, w, h); - - // Add header and fields to row - row.add(itemHeader, fieldsContainer); - inspectorList.add(row); - } - } - - private NumberField makeField(String label, Integer value){ - NumberField nf = new NumberField(label); - nf.setStep(1d); - nf.setMin(0d); - if (value != null) { nf.setValue(value.doubleValue()); } - nf.setWidth("120px"); - return nf; - } - - private void setFrameRect(String id, Double x, Double y, Double w, Double h){ - setFrameRect(id, x, y, w, h, true); - } - - private void setFrameRect(String id, Double x, Double y, Double w, Double h, boolean refreshInspector){ - if (id == null || id.isBlank()) return; - double dx = x == null ? 0 : Math.max(0, x); - double dy = y == null ? 0 : Math.max(0, y); - double dw = w == null ? 10 : Math.max(10, w); - double dh = h == null ? 10 : Math.max(10, h); - - // Set flag to indicate programmatic change before updating styles - String js = "(function(){ " + - "const el = document.getElementById('" + id + "'); " + - "if(!el) return; " + - "el._programmaticUpdate = true; " + // Flag to ignore ResizeObserver during programmatic updates - "el.style.left='" + (int)dx + "px'; " + - "el.style.top='" + (int)dy + "px'; " + - "el.style.width='" + (int)dw + "px'; " + - "el.style.height='" + (int)dh + "px'; " + - "setTimeout(() => { el._programmaticUpdate = false; }, 100); " + // Reset flag after DOM updates - "})()"; - UI.getCurrent().getPage().executeJs(js); - // Serverliste aktualisieren - upsertServerFrame(id, null, null, (int)dx, (int)dy, (int)dw, (int)dh); - // Nur Inspector neu laden wenn gewünscht (nicht bei Eingabefeld-Änderungen) - if (refreshInspector) { - refreshInspectorFromDomDeferred(); - } - } - - private void upsertServerFrame(String id, String name, String type, Integer x, Integer y, Integer width, Integer height) { - if (id == null || id.isBlank()) return; - FrameInfo found = null; - for (FrameInfo fi : serverFrames) { if (fi != null && id.equals(fi.id)) { found = fi; break; } } - if (found == null) { - found = new FrameInfo(); - found.id = id; - serverFrames.add(found); - } - if (name != null) found.name = name; - if (type != null) found.type = type; - if (x != null) found.x = x; - if (y != null) found.y = y; - if (width != null) found.width = width; - if (height != null) found.height = height; - } - - private String getFramesInspectorJs(){ - return "return (function(){\n" + - " const root = document.getElementById('pdf-canvas');\n" + - " let frames = [];\n" + - " if (root) {\n" + - " frames = Array.from(root.querySelectorAll('.canvas-frame'));\n" + - " } else {\n" + - " // Fallback: global Suche (falls #pdf-canvas z.B. durch Slot/Shadow nicht direkt gefunden wird)\n" + - " frames = Array.from(document.querySelectorAll('#pdf-canvas .canvas-frame, .pdf-canvas .canvas-frame, .canvas-frame'));\n" + - " }\n" + - " const items = frames.map(f=>{\n" + - " const cs = getComputedStyle(f);\n" + - " const id = f.id || f.getAttribute('data-frame-id') || '';\n" + - " const name = f.getAttribute('data-name') || '';\n" + - " const type = f.classList.contains('text-frame') ? 'text' : 'image';\n" + - " const x = Math.round(parseFloat(cs.left) || 0);\n" + - " const y = Math.round(parseFloat(cs.top) || 0);\n" + - " const width = Math.round(parseFloat(cs.width) || f.getBoundingClientRect().width);\n" + - " const height = Math.round(parseFloat(cs.height) || f.getBoundingClientRect().height);\n" + - " return {id, name, type, x, y, width, height};\n" + - " });\n" + - " return JSON.stringify(items);\n" + - "})()"; - } - - private static class FrameInfo { - public String id; - public String name; - public String type; - public Integer x; - public Integer y; - public Integer width; - public Integer height; - } - - private Div createPalette() { - Div palette = new Div(); - palette.addClassName("pdf-palette"); - - H3 title = new H3("Elementpalette"); - title.addClassName("palette-title"); - palette.add(title); - - // Text-Tool - Div textTool = new Div(); - textTool.addClassName("tool-item"); - textTool.getElement().setProperty("draggable", true); - textTool.getElement().setAttribute("data-tool", "text"); - Icon textIcon = VaadinIcon.FONT.create(); - Span textLabel = new Span("Text hinzufügen"); - textTool.add(textIcon, textLabel); - enableDragSource(textTool); - - // Bild-Tool - Div imageTool = new Div(); - imageTool.addClassName("tool-item"); - imageTool.getElement().setProperty("draggable", true); - imageTool.getElement().setAttribute("data-tool", "image"); - Icon imageIcon = VaadinIcon.PICTURE.create(); - Span imageLabel = new Span("Bild hinzufügen"); - imageTool.add(imageIcon, imageLabel); - enableDragSource(imageTool); - - palette.add(textTool, imageTool); - return palette; - } - - private void enableDragSource(Div toolItem) { - // HTML5-Drag-Daten setzen (Typ: text/image) - toolItem.getElement().executeJs( - "this.addEventListener('dragstart', (e) => {" + - " e.dataTransfer.setData('application/x-tool', this.dataset.tool);" + - " e.dataTransfer.effectAllowed = 'copy';" + - "});" - ); - } - - private void exportCanvasToPdf(String templateName) { - // html2canvas laden (falls nicht vorhanden) und INNERES Canvas erfassen - String js = "const el = document.getElementById('pdf-canvas');\n" + - "el.classList.add('capturing','capturing-export');\n" + - "const capture = () => html2canvas(el).then(c => c.toDataURL('image/png'));\n" + - "if (window.html2canvas) {\n" + - " return capture().then(r => { el.classList.remove('capturing','capturing-export'); return r; }).catch(err => { el.classList.remove('capturing','capturing-export'); return null; });\n" + - "}\n" + - "return new Promise(resolve => {\n" + - " const s = document.createElement('script');\n" + - " s.src = 'https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js';\n" + - " s.onload = () => capture().then(r => { el.classList.remove('capturing','capturing-export'); resolve(r); }).catch(() => { el.classList.remove('capturing','capturing-export'); resolve(null); });\n" + - " s.onerror = () => { el.classList.remove('capturing','capturing-export'); resolve(null); };\n" + - " document.head.appendChild(s);\n" + - "});"; - - PendingJavaScriptResult result = UI.getCurrent().getPage().executeJs(js); - result.then(String.class, dataUrl -> { - try { - if (dataUrl == null || !dataUrl.startsWith("data:image/png;base64,")) { - Notification.show("Fehler beim Erfassen des Canvas", 3000, Notification.Position.MIDDLE); - return; - } - String base64 = dataUrl.substring(dataUrl.indexOf(',') + 1); - byte[] pngBytes = Base64.getDecoder().decode(base64.getBytes(StandardCharsets.UTF_8)); - byte[] pdfBytes = pdfService.generatePdfFromImage(pngBytes); - - String fn = (templateName == null || templateName.isBlank() ? "canvas-export" : templateName.trim()) - .replaceAll("[\\\\/:*?\"<>|]+", "_"); - if (!fn.toLowerCase().endsWith(".pdf")) { - fn = fn + ".pdf"; - } - - StreamResource resource = new StreamResource(fn, () -> new ByteArrayInputStream(pdfBytes)); - resource.setContentType("application/pdf"); - resource.setCacheTime(0); - - StreamRegistration reg = VaadinSession.getCurrent().getResourceRegistry().registerResource(resource); - String url = reg.getResourceUri().toString(); - // Öffnet den Download direkt – i.d.R. zuverlässig und nicht durch Popup-Blocker verhindert - UI.getCurrent().getPage().open(url); - - Notification.show("PDF wurde erstellt und heruntergeladen", 3000, Notification.Position.BOTTOM_END); - } catch (Exception ex) { - Notification.show("Fehler bei PDF-Erzeugung: " + ex.getMessage(), 5000, Notification.Position.MIDDLE); - } - }); - } - - // Canvas-Komponente mit Drop-Handling und Frame-Erzeugung - class PdfCanvas extends Div { - @Serial - private static final long serialVersionUID = 1L; - - private final Div inner; - private int frameCounter = 0; - - PdfCanvas() { - addClassName("pdf-canvas-wrapper"); - - // Inneres Canvas-Element (relative, A4-Seitenverhältnis) - inner = new Div(); - inner.addClassName("pdf-canvas"); - inner.setId("pdf-canvas"); - add(inner); - - enableDrop(inner); - enableCanvasDeactivation(inner); - } - - private void refreshInspectorAfterClientFlush() { - UI ui = UI.getCurrent(); - if (ui == null) { - PDFBuilderView.this.refreshInspectorFromDom(); - return; - } - ui.beforeClientResponse(PdfCanvas.this, ctx -> PDFBuilderView.this.refreshInspectorFromDom()); - } - - private void enableDrop(Div target) { - // Dragover/Drops auf dem Canvas erfassen und an Server melden - target.getElement().executeJs( - "const canvas = this;" + - "canvas.addEventListener('dragover', e => { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; });\n" + - "canvas.addEventListener('drop', e => {\n" + - " e.preventDefault();\n" + - " const rect = canvas.getBoundingClientRect();\n" + - " const x = Math.round(e.clientX - rect.left);\n" + - " const y = Math.round(e.clientY - rect.top);\n" + - " const tool = e.dataTransfer.getData('application/x-tool');\n" + - " if (canvas.parentElement && canvas.parentElement.$server) {\n" + - " canvas.parentElement.$server.onDrop(tool, x, y);\n" + - " }\n" + - "});" - ); - } - - private void enableCanvasDeactivation(Div target) { - target.getElement().executeJs( - "const canvas = this;\n" + - "function deactivateAll(){\n" + - " canvas.querySelectorAll('.canvas-frame.active').forEach(f => f.classList.remove('active'));\n" + - " canvas.querySelectorAll('.text-editor').forEach(ed => { if (ed.blur) ed.blur(); });\n" + - "}\n" + - "canvas.addEventListener('mousedown', (e) => {\n" + - " const el = e.target;\n" + - " if (el === canvas || !el.closest('.canvas-frame')) { deactivateAll(); }\n" + - "});\n" + - "canvas.addEventListener('touchstart', (e) => {\n" + - " const el = e.target;\n" + - " if (el === canvas || !el.closest('.canvas-frame')) { deactivateAll(); }\n" + - "}, { passive: true });" - ); - } - - @com.vaadin.flow.component.ClientCallable - private void onDrop(String tool, int x, int y) { - if (Objects.equals(tool, "text")) { - showElementNameDialog("Text", () -> addTextFrame(x, y)); - } else if (Objects.equals(tool, "image")) { - showElementNameDialog("Bild", () -> promptImageUpload(x, y)); - } - } - - @com.vaadin.flow.component.ClientCallable - private void onFrameChanged() { - // Nach Drag/Resize: Inspector-Seite aktualisieren - PDFBuilderView.this.refreshInspectorFromDomDeferred(); - } - - private void addTextFrame(int x, int y) { - Div frame = new Div(); - frame.addClassName("canvas-frame"); - frame.addClassName("text-frame"); - String id = "frame-" + (++frameCounter); - frame.setId(id); - frame.getElement().setAttribute("data-frame-id", id); - frame.getElement().setAttribute("data-type", "text"); - frame.getElement().setAttribute("data-name", currentElementName != null ? currentElementName : "Text"); - frame.getElement().setAttribute("data-include-content", String.valueOf(currentIncludeContent)); - frame.getStyle().set("left", x + "px"); - frame.getStyle().set("top", y + "px"); - frame.getStyle().set("width", "300px"); - frame.getStyle().set("height", "140px"); - - // Move-Handle (oben links) - Div moveHandle = new Div(); - moveHandle.addClassName("frame-handle-move"); - moveHandle.setText("⠿"); - - // Toolbar - HorizontalLayout toolbar = new HorizontalLayout(); - toolbar.addClassName("text-toolbar"); - toolbar.setPadding(false); - toolbar.setSpacing(true); - - Button bBold = new Button(new Icon(VaadinIcon.BOLD)); - bBold.addThemeVariants(ButtonVariant.LUMO_TERTIARY_INLINE); - Button bItalic = new Button(new Icon(VaadinIcon.ITALIC)); - bItalic.addThemeVariants(ButtonVariant.LUMO_TERTIARY_INLINE); - Button bUnderline = new Button(new Icon(VaadinIcon.UNDERLINE)); - bUnderline.addThemeVariants(ButtonVariant.LUMO_TERTIARY_INLINE); - Button bSmaller = new Button(new Icon(VaadinIcon.MINUS)); - bSmaller.addThemeVariants(ButtonVariant.LUMO_TERTIARY_INLINE); - Button bLarger = new Button(new Icon(VaadinIcon.PLUS)); - bLarger.addThemeVariants(ButtonVariant.LUMO_TERTIARY_INLINE); - Button cBlack = new Button(); cBlack.getElement().getThemeList().add("tertiary-inline"); cBlack.getStyle().set("background","#000"); cBlack.getStyle().set("width","16px"); cBlack.getStyle().set("height","16px"); - Button cRed = new Button(); cRed.getElement().getThemeList().add("tertiary-inline"); cRed.getStyle().set("background","#e11"); cRed.getStyle().set("width","16px"); cRed.getStyle().set("height","16px"); - Button cBlue = new Button(); cBlue.getElement().getThemeList().add("tertiary-inline"); cBlue.getStyle().set("background","#16c"); cBlue.getStyle().set("width","16px"); cBlue.getStyle().set("height","16px"); - - Div editor = new Div(); - editor.addClassName("text-editor"); - editor.getElement().setAttribute("contenteditable", "true"); - editor.getElement().setAttribute("tabindex", "0"); - editor.getStyle().set("padding", "8px"); - editor.getStyle().set("font-size", "16px"); - editor.getStyle().set("line-height", "1.3"); - editor.setText("Text hier eingeben…"); - - // Button-Aktionen (clientseitig auf Selektion) - bBold.addClickListener(e -> editor.getElement().executeJs("document.execCommand('bold')")); - bItalic.addClickListener(e -> editor.getElement().executeJs("document.execCommand('italic')")); - bUnderline.addClickListener(e -> editor.getElement().executeJs("document.execCommand('underline')")); - bSmaller.addClickListener(e -> editor.getElement().executeJs( - "const s = parseInt(getComputedStyle(this).fontSize)||16; this.style.fontSize=(Math.max(8,s-2))+'px';")); - bLarger.addClickListener(e -> editor.getElement().executeJs( - "const s = parseInt(getComputedStyle(this).fontSize)||16; this.style.fontSize=(s+2)+'px';")); - cBlack.addClickListener(e -> editor.getElement().executeJs("document.execCommand('foreColor', false, '#000000')")); - cRed.addClickListener(e -> editor.getElement().executeJs("document.execCommand('foreColor', false, '#cc1111')")); - cBlue.addClickListener(e -> editor.getElement().executeJs("document.execCommand('foreColor', false, '#1166cc')")); - - toolbar.add(bBold, bItalic, bUnderline, bSmaller, bLarger, cBlack, cRed, cBlue); - - frame.add(moveHandle, toolbar, editor); - // Frame dem inneren Canvas hinzufügen - inner.add(frame); - // Drag-to-move aktivieren - enableFrameDragging(frame); - // Aktivierungsverhalten - enableFrameActivation(frame); - // Serverliste sofort updaten und rendern, damit der Eintrag direkt erscheint - upsertServerFrame(id, currentElementName, "text", x, y, 300, 140); - renderInspectorItems(serverFrames); - // Zusätzlich clientseitig deferred refresher, um DOM-Werte zu synchronisieren - PDFBuilderView.this.refreshInspectorFromDomDeferred(); - } - - private void promptImageUpload(int x, int y) { - Dialog dlg = new Dialog(); - dlg.setHeaderTitle("Bild hochladen"); - dlg.setModal(true); - MemoryBuffer buffer = new MemoryBuffer(); - Upload upload = new Upload(buffer); - upload.setAcceptedFileTypes("image/*"); - upload.setMaxFiles(1); - upload.addSucceededListener(succeededEvent -> { - try { - byte[] bytes = buffer.getInputStream().readAllBytes(); - addImageFrame(x, y, bytes, succeededEvent.getFileName()); - dlg.close(); - } catch (IOException ex) { - Notification.show("Upload fehlgeschlagen: " + ex.getMessage(), 5000, Notification.Position.MIDDLE); - } - }); - Button cancel = new Button("Abbrechen", e -> dlg.close()); - VerticalLayout content = new VerticalLayout(new Span("Bitte eine Bilddatei auswählen."), upload); - content.setPadding(false); - content.setSpacing(true); - dlg.add(content); - dlg.getFooter().add(cancel); - dlg.open(); - } - - private void addImageFrame(int x, int y, byte[] imageBytes, String filename) { - Div frame = new Div(); - frame.addClassName("canvas-frame"); - frame.addClassName("image-frame"); - String id = "frame-" + (++frameCounter); - frame.setId(id); - frame.getElement().setAttribute("data-frame-id", id); - frame.getElement().setAttribute("data-type", "image"); - frame.getElement().setAttribute("data-name", currentElementName != null ? currentElementName : "Bild"); - frame.getElement().setAttribute("data-include-content", String.valueOf(currentIncludeContent)); - frame.getStyle().set("left", x + "px"); - frame.getStyle().set("top", y + "px"); - frame.getStyle().set("width", "260px"); - frame.getStyle().set("height", "180px"); - - String base64 = Base64.getEncoder().encodeToString(imageBytes); - Image img = new Image("data:image/png;base64," + base64, filename == null ? "Bild" : filename); - img.getStyle().set("width", "100%"); - img.getStyle().set("height", "100%"); - img.getStyle().set("object-fit", "contain"); - - // Move-Handle (oben links) - Div moveHandle = new Div(); - moveHandle.addClassName("frame-handle-move"); - moveHandle.setText("⠿"); - - // Nur Bild (Toolbar für Bilder aktuell nicht notwendig) - frame.add(moveHandle, img); - inner.add(frame); - // Drag-to-move aktivieren - enableFrameDragging(frame); - // Aktivierungsverhalten - enableFrameActivation(frame); - // Serverliste sofort updaten und rendern - upsertServerFrame(id, currentElementName, "image", x, y, 260, 180); - renderInspectorItems(serverFrames); - // Zusätzlich deferred refresher - PDFBuilderView.this.refreshInspectorFromDomDeferred(); - } - - private void enableFrameDragging(Div frame) { - frame.getElement().executeJs( - "const frame = this;\n" + - "const root = document.getElementById('pdf-canvas');\n" + - "const canvas = root;\n" + - "let startX = 0, startY = 0, startLeft = 0, startTop = 0;\n" + - "frame.__dragMoved = false;\n" + - "frame.__downOutsideEditor = false;\n" + - "function pxToInt(v){ const n = parseInt(v); return isNaN(n)?0:n; }\n" + - "function placeCaretAtEnd(el){ try{ const range=document.createRange(); range.selectNodeContents(el); range.collapse(false); const sel=window.getSelection(); sel.removeAllRanges(); sel.addRange(range);}catch(err){} }\n" + - "function onDown(e){\n" + - " const target = e.target;\n" + - " const isHandle = target && target.closest && target.closest('.frame-handle-move');\n" + - " // Toolbar-Klicks nie zum Ziehen verwenden\n" + - " if (target && target.closest && target.closest('.text-toolbar')) { return; }\n" + - " // Klick im Editor soll immer fokussieren und nicht ziehen\n" + - " const editor = frame.querySelector('.text-editor');\n" + - " const clickedInEditor = target && target.closest && target.closest('.text-editor');\n" + - " if (!isHandle && clickedInEditor) { return; }\n" + - " const p = (e.touches && e.touches[0]) ? e.touches[0] : e;\n" + - " // Nicht ziehen, wenn in der Nähe der Resize-Kanten geklickt wurde (damit Resize funktioniert)\n" + - " const rect = frame.getBoundingClientRect();\n" + - " const offsetX = p.clientX - rect.left;\n" + - " const offsetY = p.clientY - rect.top;\n" + - " const edge = 14;\n" + - " if (!isHandle && (offsetX < edge || offsetY < edge || (rect.width - offsetX) < edge || (rect.height - offsetY) < edge)) { return; }\n" + - " frame.__downOutsideEditor = !isHandle;\n" + - " e.preventDefault();\n" + - " startX = p.clientX; startY = p.clientY;\n" + - " startLeft = pxToInt(frame.style.left);\n" + - " startTop = pxToInt(frame.style.top);\n" + - " frame.__dragMoved = false;\n" + - " frame.style.zIndex = String((parseInt(frame.style.zIndex)||0) + 1);\n" + - " document.addEventListener('mousemove', onMove);\n" + - " document.addEventListener('mouseup', onUp, { once: true });\n" + - " document.addEventListener('touchmove', onMove, { passive: false });\n" + - " document.addEventListener('touchend', onUp, { once: true });\n" + - "}\n" + - "function onMove(e){\n" + - " const p = (e.touches && e.touches[0]) ? e.touches[0] : e;\n" + - " if (!p) return;\n" + - " e.preventDefault();\n" + - " const dx = p.clientX - startX;\n" + - " const dy = p.clientY - startY;\n" + - " if (Math.abs(dx) > 2 || Math.abs(dy) > 2) { frame.__dragMoved = true; }\n" + - " const rootRect = root.getBoundingClientRect();\n" + - " const maxLeft = rootRect.width - frame.offsetWidth;\n" + - " const maxTop = rootRect.height - frame.offsetHeight;\n" + - " let newLeft = Math.min(Math.max(0, startLeft + dx), maxLeft);\n" + - " let newTop = Math.min(Math.max(0, startTop + dy), maxTop);\n" + - " frame.style.left = Math.round(newLeft) + 'px';\n" + - " frame.style.top = Math.round(newTop) + 'px';\n" + - "}\n" + - "function onUp(){\n" + - " document.removeEventListener('mousemove', onMove);\n" + - " document.removeEventListener('touchmove', onMove);\n" + - " // Plain Click auf Frame (nicht im Editor, kein Drag): Editor fokussieren\n" + - " if (!frame.__dragMoved && frame.__downOutsideEditor){ const ed = frame.querySelector('.text-editor'); if (ed){ ed.focus(); placeCaretAtEnd(ed); } }\n" + - " // Inspector informieren\n" + - " try { if (canvas && canvas.parentElement && canvas.parentElement.$server && canvas.parentElement.$server.onFrameChanged) { canvas.parentElement.$server.onFrameChanged(); } } catch(e){}\n" + - " // dragMoved-Flag nach dem Click-Zyklus zurücksetzen\n" + - " setTimeout(()=>{ frame.__dragMoved = false; frame.__downOutsideEditor = false; }, 0);\n" + - "}\n" + - "frame.addEventListener('mousedown', onDown);\n" + - "frame.addEventListener('touchstart', onDown, { passive: false });\n" + - "// Größenänderungen beobachten (ResizeObserver)\n" + - "if (window.ResizeObserver) { const ro = new ResizeObserver(()=>{ try { if (!frame._programmaticUpdate && canvas && canvas.parentElement && canvas.parentElement.$server && canvas.parentElement.$server.onFrameChanged) { canvas.parentElement.$server.onFrameChanged(); } } catch(e){} }); ro.observe(frame); }\n" - ); - } - - private void enableFrameActivation(Div frame) { - frame.getElement().executeJs( - "const frame = this;\n" + - "function setActive(){\n" + - " const root = document.getElementById('pdf-canvas');\n" + - " if (!root) return;\n" + - " root.querySelectorAll('.canvas-frame.active').forEach(f => f.classList.remove('active'));\n" + - " frame.classList.add('active');\n" + - "}\n" + - "frame.addEventListener('mousedown', ()=>{ setActive(); });\n" + - "frame.addEventListener('touchstart', ()=>{ setActive(); }, { passive: true });\n" + - "const editor = frame.querySelector('.text-editor');\n" + - "if (editor){\n" + - " editor.addEventListener('focus', ()=> setActive());\n" + - " editor.addEventListener('mousedown', ()=> setActive());\n" + - "}\n" + - "function placeCaretAtEnd(el){ try{ const range=document.createRange(); range.selectNodeContents(el); range.collapse(false); const sel=window.getSelection(); sel.removeAllRanges(); sel.addRange(range);}catch(err){} }\n" + - "frame.addEventListener('click', (e)=>{\n" + - " const t = e.target;\n" + - " if (t && t.closest && t.closest('.text-toolbar')) return;\n" + - " const ed = frame.querySelector('.text-editor');\n" + - " if (!ed) return;\n" + - " // Wenn nicht direkt im Editor geklickt und kein Drag stattfand: Editor fokussieren\n" + - " if (!(t && t.closest && t.closest('.text-editor')) && !frame.__dragMoved){\n" + - " ed.focus();\n" + - " placeCaretAtEnd(ed);\n" + - " }\n" + - "});" - ); - } - } -} diff --git a/src/main/java/de/assecutor/votianlt/repository/JobRepository.java b/src/main/java/de/assecutor/votianlt/repository/JobRepository.java index 1b85d3c..7941dcf 100644 --- a/src/main/java/de/assecutor/votianlt/repository/JobRepository.java +++ b/src/main/java/de/assecutor/votianlt/repository/JobRepository.java @@ -84,4 +84,9 @@ public interface JobRepository extends MongoRepository { * Zählt alle Entwürfe */ long countByIsDraftTrue(); + + /** + * Findet alle Aufträge, die einem bestimmten App-Nutzer zugewiesen sind + */ + List findByAppUser(String appUser); } diff --git a/src/main/java/de/assecutor/votianlt/repository/PdfTemplateRepository.java b/src/main/java/de/assecutor/votianlt/repository/PdfTemplateRepository.java deleted file mode 100644 index 6a64fa0..0000000 --- a/src/main/java/de/assecutor/votianlt/repository/PdfTemplateRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package de.assecutor.votianlt.repository; - -import de.assecutor.votianlt.model.PdfTemplate; -import org.springframework.data.mongodb.repository.MongoRepository; - -import java.util.Optional; - -public interface PdfTemplateRepository extends MongoRepository { - Optional findByName(String name); -}