Erweiterungen
This commit is contained in:
@@ -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 */
|
|
||||||
@@ -3,8 +3,11 @@ package de.assecutor.votianlt.controller;
|
|||||||
import de.assecutor.votianlt.dto.AppLoginRequest;
|
import de.assecutor.votianlt.dto.AppLoginRequest;
|
||||||
import de.assecutor.votianlt.dto.AppLoginResponse;
|
import de.assecutor.votianlt.dto.AppLoginResponse;
|
||||||
import de.assecutor.votianlt.model.AppUser;
|
import de.assecutor.votianlt.model.AppUser;
|
||||||
|
import de.assecutor.votianlt.model.Job;
|
||||||
import de.assecutor.votianlt.pages.service.AppUserService;
|
import de.assecutor.votianlt.pages.service.AppUserService;
|
||||||
import de.assecutor.votianlt.repository.AppUserRepository;
|
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.MessageMapping;
|
||||||
import org.springframework.messaging.handler.annotation.SendTo;
|
import org.springframework.messaging.handler.annotation.SendTo;
|
||||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||||
@@ -14,6 +17,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,6 +25,7 @@ import java.util.Map;
|
|||||||
* Provides endpoints for sending and receiving messages via WebSocket/STOMP.
|
* Provides endpoints for sending and receiving messages via WebSocket/STOMP.
|
||||||
*/
|
*/
|
||||||
@Controller
|
@Controller
|
||||||
|
@Slf4j
|
||||||
public class MessageController {
|
public class MessageController {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@@ -32,16 +37,22 @@ public class MessageController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private AppUserService appUserService;
|
private AppUserService appUserService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private JobRepository jobRepository;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles messages sent to /app/message and broadcasts them to all subscribers of /topic/messages
|
* Handles messages sent to /app/message and broadcasts them to all subscribers of /topic/messages
|
||||||
*/
|
*/
|
||||||
@MessageMapping("/message")
|
@MessageMapping("/message")
|
||||||
@SendTo("/topic/messages")
|
@SendTo("/topic/messages")
|
||||||
public Map<String, Object> handleMessage(Map<String, Object> message) {
|
public Map<String, Object> handleMessage(Map<String, Object> message) {
|
||||||
|
log.info("STOMP Endpoint '/app/message' called with data: {}", message);
|
||||||
|
|
||||||
// Add timestamp to the message
|
// Add timestamp to the message
|
||||||
message.put("timestamp", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
|
message.put("timestamp", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
|
||||||
message.put("processed", true);
|
message.put("processed", true);
|
||||||
|
|
||||||
|
log.info("STOMP Response for '/app/message' sent to '/topic/messages': {}", message);
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,9 +62,12 @@ public class MessageController {
|
|||||||
@MessageMapping("/job/status")
|
@MessageMapping("/job/status")
|
||||||
@SendTo("/topic/job-updates")
|
@SendTo("/topic/job-updates")
|
||||||
public Map<String, Object> handleJobStatusUpdate(Map<String, Object> jobUpdate) {
|
public Map<String, Object> handleJobStatusUpdate(Map<String, Object> 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("timestamp", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
|
||||||
jobUpdate.put("source", "app");
|
jobUpdate.put("source", "app");
|
||||||
|
|
||||||
|
log.info("STOMP Response for '/app/job/status' sent to '/topic/job-updates': {}", jobUpdate);
|
||||||
return jobUpdate;
|
return jobUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,9 +77,12 @@ public class MessageController {
|
|||||||
@MessageMapping("/device/location")
|
@MessageMapping("/device/location")
|
||||||
@SendTo("/topic/device-locations")
|
@SendTo("/topic/device-locations")
|
||||||
public Map<String, Object> handleDeviceLocation(Map<String, Object> locationUpdate) {
|
public Map<String, Object> handleDeviceLocation(Map<String, Object> 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("timestamp", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
|
||||||
locationUpdate.put("processed", true);
|
locationUpdate.put("processed", true);
|
||||||
|
|
||||||
|
log.info("STOMP Response for '/app/device/location' sent to '/topic/device-locations': {}", locationUpdate);
|
||||||
return locationUpdate;
|
return locationUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +96,9 @@ public class MessageController {
|
|||||||
"type", "notification"
|
"type", "notification"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
log.info("Sending notification to user '{}': {}", username, notification);
|
||||||
messagingTemplate.convertAndSendToUser(username, "/queue/notifications", 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"
|
"type", "broadcast"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
log.info("Sending broadcast message: {}", broadcast);
|
||||||
messagingTemplate.convertAndSend("/topic/broadcasts", broadcast);
|
messagingTemplate.convertAndSend("/topic/broadcasts", broadcast);
|
||||||
|
log.info("Broadcast message sent to '/topic/broadcasts'");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -103,21 +124,65 @@ public class MessageController {
|
|||||||
@MessageMapping("/auth/login")
|
@MessageMapping("/auth/login")
|
||||||
@SendToUser("/queue/auth")
|
@SendToUser("/queue/auth")
|
||||||
public AppLoginResponse handleAppLogin(AppLoginRequest request) {
|
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
|
if (request == null || request.getEmail() == null || request.getPassword() == null
|
||||||
|| request.getEmail().isBlank() || request.getPassword().isBlank()) {
|
|| 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());
|
AppUser user = appUserRepository.findByEmail(request.getEmail());
|
||||||
if (user == null) {
|
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());
|
boolean ok = appUserService.verifyPassword(request.getPassword(), user.getPassword());
|
||||||
if (!ok) {
|
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<Job> handleGetAssignedJobs(Map<String, Object> 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<Job> 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<PdfTemplate> 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<PdfTemplate> findByName(String name) {
|
|
||||||
return repository.findByName(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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.base.ui.component.ViewToolbar;
|
||||||
import de.assecutor.votianlt.pages.service.AddCustomerService;
|
import de.assecutor.votianlt.pages.service.AddCustomerService;
|
||||||
import de.assecutor.votianlt.model.Customer;
|
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 jakarta.annotation.security.RolesAllowed;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -52,6 +54,7 @@ public class AddJobView extends Main {
|
|||||||
private final AddJobService addJobService;
|
private final AddJobService addJobService;
|
||||||
private final CustomerService customerService;
|
private final CustomerService customerService;
|
||||||
private final AddCustomerService addCustomerService;
|
private final AddCustomerService addCustomerService;
|
||||||
|
private final AppUserService appUserService;
|
||||||
|
|
||||||
// Customer selection
|
// Customer selection
|
||||||
private ComboBox<String> customerSelection;
|
private ComboBox<String> customerSelection;
|
||||||
@@ -85,7 +88,7 @@ public class AddJobView extends Main {
|
|||||||
|
|
||||||
// Digital processing
|
// Digital processing
|
||||||
private Checkbox digitalProcessing;
|
private Checkbox digitalProcessing;
|
||||||
private ComboBox<String> appUser;
|
private ComboBox<AppUser> appUser;
|
||||||
|
|
||||||
// Price field
|
// Price field
|
||||||
private TextField price;
|
private TextField price;
|
||||||
@@ -122,10 +125,14 @@ public class AddJobView extends Main {
|
|||||||
// Mapping für die Anzeige-Labels der Kunden zur Entität
|
// Mapping für die Anzeige-Labels der Kunden zur Entität
|
||||||
private final Map<String, Customer> customerLabelToEntity = new LinkedHashMap<>();
|
private final Map<String, Customer> customerLabelToEntity = new LinkedHashMap<>();
|
||||||
|
|
||||||
public AddJobView(AddJobService addJobService, AddCustomerService addCustomerService, CustomerService customerService) {
|
// Available app users for the current user
|
||||||
|
private List<AppUser> availableAppUsers;
|
||||||
|
|
||||||
|
public AddJobView(AddJobService addJobService, AddCustomerService addCustomerService, CustomerService customerService, AppUserService appUserService) {
|
||||||
this.addJobService = addJobService;
|
this.addJobService = addJobService;
|
||||||
this.addCustomerService = addCustomerService;
|
this.addCustomerService = addCustomerService;
|
||||||
this.customerService = customerService;
|
this.customerService = customerService;
|
||||||
|
this.appUserService = appUserService;
|
||||||
initializeComponents();
|
initializeComponents();
|
||||||
setupLayout();
|
setupLayout();
|
||||||
setupValidation();
|
setupValidation();
|
||||||
@@ -267,7 +274,11 @@ public class AddJobView extends Main {
|
|||||||
// Digital processing
|
// Digital processing
|
||||||
digitalProcessing = new Checkbox("Digitale Abwicklung per App");
|
digitalProcessing = new Checkbox("Digitale Abwicklung per App");
|
||||||
appUser = new ComboBox<>("App-Nutzer");
|
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...");
|
appUser.setPlaceholder("App-Nutzer auswählen...");
|
||||||
|
|
||||||
// Price field
|
// Price field
|
||||||
@@ -762,7 +773,22 @@ public class AddJobView extends Main {
|
|||||||
binder.bind(deliveryAddressAddition, Job::getDeliveryAddressAddition, Job::setDeliveryAddressAddition);
|
binder.bind(deliveryAddressAddition, Job::getDeliveryAddressAddition, Job::setDeliveryAddressAddition);
|
||||||
|
|
||||||
binder.bind(digitalProcessing, Job::isDigitalProcessing, Job::setDigitalProcessing);
|
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
|
// Set up validation triggers and visual styling
|
||||||
setupValidationTriggers();
|
setupValidationTriggers();
|
||||||
@@ -938,6 +964,14 @@ public class AddJobView extends Main {
|
|||||||
|
|
||||||
// Validate all required fields using the binder
|
// Validate all required fields using the binder
|
||||||
if (binder.writeBeanIfValid(job)) {
|
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)
|
// 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
|
// Definition: Ein Cargo-Item gilt nur als gefüllt, wenn eine Beschreibung vorhanden ist
|
||||||
List<CargoItem> cargoFilled = cargoItemsState.stream()
|
List<CargoItem> cargoFilled = cargoItemsState.stream()
|
||||||
|
|||||||
@@ -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<FrameInfo> serverFrames = new ArrayList<>();
|
|
||||||
// Track which items are collapsed by their ID
|
|
||||||
private final Set<String> 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<FrameInfo> frames = objectMapper.readValue(safeJson, new TypeReference<List<FrameInfo>>(){});
|
|
||||||
// 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<FrameInfo> 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" +
|
|
||||||
"});"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -84,4 +84,9 @@ public interface JobRepository extends MongoRepository<Job, ObjectId> {
|
|||||||
* Zählt alle Entwürfe
|
* Zählt alle Entwürfe
|
||||||
*/
|
*/
|
||||||
long countByIsDraftTrue();
|
long countByIsDraftTrue();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Findet alle Aufträge, die einem bestimmten App-Nutzer zugewiesen sind
|
||||||
|
*/
|
||||||
|
List<Job> findByAppUser(String appUser);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<PdfTemplate, String> {
|
|
||||||
Optional<PdfTemplate> findByName(String name);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user