diff --git a/app/lib/models/job.dart b/app/lib/models/job.dart index b56103a..07a111c 100644 --- a/app/lib/models/job.dart +++ b/app/lib/models/job.dart @@ -258,6 +258,40 @@ class Job { return (a.taskOrder ?? 0).compareTo(b.taskOrder ?? 0); } + // The tasks embedded in deliveryStations are snapshots from job creation + // and never reflect later completions. The top-level tasks list comes from + // the server's task collection and carries the current completion state, + // so overlay it onto the station tasks by id. + final topLevelTasksRaw = json['tasks'] ?? jobJson['tasks']; + if (deliveryStations.isNotEmpty && topLevelTasksRaw is List) { + final freshTasksById = {}; + for (final rawTask in topLevelTasksRaw) { + if (rawTask is! Map) { + continue; + } + try { + final task = Task.fromJson(Map.from(rawTask)); + if (task.id.isNotEmpty) { + freshTasksById[task.id] = task; + } + } catch (_) { + // Ignore unparseable entries; the station snapshot stays in place. + } + } + for (final station in deliveryStations) { + for (var i = 0; i < station.tasks.length; i++) { + final fresh = freshTasksById[station.tasks[i].id]; + if (fresh != null && fresh.completed && !station.tasks[i].completed) { + station.tasks[i] = station.tasks[i].copyWith( + completed: true, + completedAt: fresh.completedAt, + completedBy: fresh.completedBy, + ); + } + } + } + } + // Parse tasks array List tasks = []; if (deliveryStations.isNotEmpty) { diff --git a/app/lib/services/database_service.dart b/app/lib/services/database_service.dart index 579d96e..7388c28 100644 --- a/app/lib/services/database_service.dart +++ b/app/lib/services/database_service.dart @@ -116,9 +116,16 @@ class DatabaseService { final jobBox = _store!.box(); final taskStatusBox = _store!.box(); - // Clear existing jobs and related task statuses before inserting new ones + // Clear existing jobs before inserting new ones. Task statuses are NOT + // cleared wholesale: locally completed tasks may not be confirmed by the + // server yet (offline buffering), so only statuses of tasks that no + // longer exist in any job are removed below. jobBox.removeAll(); - taskStatusBox.removeAll(); + + final existingStatuses = { + for (final entity in taskStatusBox.getAll()) entity.taskId: entity, + }; + final knownTaskIds = {}; // Save new jobs for (final job in jobs) { @@ -134,7 +141,8 @@ class DatabaseService { // Also persist task completion states from JSON (adopt status on load) // Only set completed=true entries to avoid overwriting local progress with false for (final task in normalized.tasks) { - if (task.completed) { + knownTaskIds.add(task.id); + if (task.completed && !existingStatuses.containsKey(task.id)) { final taskStatusEntity = TaskStatusEntity( taskId: task.id, completed: true, @@ -147,6 +155,13 @@ class DatabaseService { } } + // Drop statuses of tasks that no longer belong to any assigned job + for (final entry in existingStatuses.entries) { + if (!knownTaskIds.contains(entry.key)) { + taskStatusBox.remove(entry.value.id); + } + } + developer.log( 'Jobs and task statuses saved successfully', name: 'DatabaseService', diff --git a/backend/pom.xml b/backend/pom.xml index 72b829f..47ff8a6 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -11,7 +11,7 @@ jar - 0.9.17 + 0.9.18 21 21 21 @@ -184,10 +184,11 @@ 5.0.5 - + - org.springframework.ai - spring-ai-starter-model-openai + com.anthropic + anthropic-java + 2.34.0 diff --git a/backend/src/main/bundles/prod.bundle b/backend/src/main/bundles/prod.bundle index 76ac393..b250d4f 100644 Binary files a/backend/src/main/bundles/prod.bundle and b/backend/src/main/bundles/prod.bundle differ diff --git a/backend/src/main/java/de/assecutor/votianlt/ai/config/LlmConfig.java b/backend/src/main/java/de/assecutor/votianlt/ai/config/LlmConfig.java index cc9ffdb..895c967 100644 --- a/backend/src/main/java/de/assecutor/votianlt/ai/config/LlmConfig.java +++ b/backend/src/main/java/de/assecutor/votianlt/ai/config/LlmConfig.java @@ -1,124 +1,57 @@ package de.assecutor.votianlt.ai.config; +import com.anthropic.client.AnthropicClient; +import com.anthropic.client.okhttp.AnthropicOkHttpClient; +import com.anthropic.models.messages.MessageCountTokensParams; +import com.anthropic.models.messages.MessageTokensCount; import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; -import java.net.HttpURLConnection; -import java.net.URI; -import java.net.URL; -import java.util.Base64; - /** - * Configuration for LLM integration via LM Studio. The LM Studio instance - * exposes an OpenAI-compatible API at {@code /v1/chat/completions}. + * Configuration for LLM integration via the Anthropic Claude API. */ @Configuration @Slf4j public class LlmConfig { - @Value("${app.ai.lmstudio.base-url}") - private String lmstudioBaseUrl; + @Value("${app.ai.anthropic.api-key}") + private String apiKey; - @Value("${app.ai.lmstudio.model}") - private String lmstudioModel; - - @Value("${app.ai.lmstudio.htaccess-username}") - private String lmstudioHtaccessUsername; - - @Value("${app.ai.lmstudio.htaccess-password}") - private String lmstudioHtaccessPassword; + @Value("${app.ai.anthropic.model:claude-opus-4-8}") + private String model; @PostConstruct public void logConfig() { log.info("=== LLM Configuration ==="); - log.info("Provider: lmstudio"); - log.info("Base URL: {}", lmstudioBaseUrl); - log.info("Model: {}", lmstudioModel); - log.info("HTACCESS auth: {}", hasHtaccessCredentials() ? "configured" : "not configured"); - testConnection(lmstudioBaseUrl, lmstudioModel); + log.info("Provider: Anthropic Claude"); + log.info("Model: {}", model); + log.info("API key: {}", apiKey != null && !apiKey.isBlank() ? "configured" : "NOT configured"); + testConnection(); } - private void testConnection(String baseUrl, String model) { - log.info("Testing LLM connection to: {}", baseUrl); - - // Test 1: Models endpoint - testEndpoint(baseUrl + "/v1/models", "GET", null); - - // Test 2: Chat completions (no streaming) - String testPayload = "{\"model\":\"" + model - + "\",\"messages\":[{\"role\":\"user\",\"content\":\"ping\"}],\"max_tokens\":1,\"stream\":false}"; - testEndpoint(baseUrl + "/v1/chat/completions", "POST", testPayload); - } - - private void testEndpoint(String endpoint, String method, String payload) { + /** + * Validate API key and connectivity using the free count_tokens endpoint (no + * tokens are billed). + */ + private void testConnection() { + if (apiKey == null || apiKey.isBlank()) { + log.warn("Skipping Claude connection test - no API key configured"); + return; + } try { - log.info("Testing endpoint: {} {}", method, endpoint); - URL url = URI.create(endpoint).toURL(); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setRequestMethod(method); - connection.setConnectTimeout(5000); - connection.setReadTimeout(10000); - - if (hasHtaccessCredentials()) { - String credentials = lmstudioHtaccessUsername + ":" + lmstudioHtaccessPassword; - String encoded = Base64.getEncoder().encodeToString(credentials.getBytes()); - connection.setRequestProperty("Authorization", "Basic " + encoded); - } - - if (payload != null) { - connection.setDoOutput(true); - connection.setRequestProperty("Content-Type", "application/json"); - try (var os = connection.getOutputStream()) { - os.write(payload.getBytes()); - } - } - - int responseCode = connection.getResponseCode(); - String responseMessage = connection.getResponseMessage(); - - if (responseCode >= 200 && responseCode < 300) { - log.info(" -> SUCCESS (HTTP {} {})", responseCode, responseMessage); - } else { - String errorBody = ""; - try (var is = connection.getErrorStream()) { - if (is != null) { - errorBody = new String(is.readAllBytes()); - } - } - log.warn(" -> HTTP {} {} - {}", responseCode, responseMessage, errorBody); - } - connection.disconnect(); - } catch (java.net.ConnectException e) { - log.error(" -> FAILED - Connection refused: {}", e.getMessage()); - } catch (java.net.SocketTimeoutException e) { - log.error(" -> FAILED - Timeout: {}", e.getMessage()); - } catch (java.net.UnknownHostException e) { - log.error(" -> FAILED - Unknown host: {}", e.getMessage()); + AnthropicClient client = AnthropicOkHttpClient.builder().apiKey(apiKey).build(); + MessageTokensCount count = client.messages() + .countTokens(MessageCountTokensParams.builder().model(model).addUserMessage("ping").build()); + log.info("Claude connection test -> SUCCESS (count_tokens returned {} input tokens)", + count.inputTokens()); } catch (Exception e) { - log.error(" -> FAILED: {} - {}", e.getClass().getSimpleName(), e.getMessage()); + log.error("Claude connection test -> FAILED: {} - {}", e.getClass().getSimpleName(), e.getMessage()); } } - public String getBaseUrl() { - return lmstudioBaseUrl; - } - public String getModel() { - return lmstudioModel; - } - - public boolean hasHtaccessCredentials() { - return lmstudioHtaccessUsername != null && !lmstudioHtaccessUsername.isBlank() - && lmstudioHtaccessPassword != null && !lmstudioHtaccessPassword.isBlank(); - } - - public String getLmstudioHtaccessUsername() { - return lmstudioHtaccessUsername; - } - - public String getLmstudioHtaccessPassword() { - return lmstudioHtaccessPassword; + return model; } } diff --git a/backend/src/main/java/de/assecutor/votianlt/ai/service/LlmRestClient.java b/backend/src/main/java/de/assecutor/votianlt/ai/service/LlmRestClient.java index 6cbeb08..226f736 100644 --- a/backend/src/main/java/de/assecutor/votianlt/ai/service/LlmRestClient.java +++ b/backend/src/main/java/de/assecutor/votianlt/ai/service/LlmRestClient.java @@ -1,24 +1,20 @@ package de.assecutor.votianlt.ai.service; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.anthropic.client.AnthropicClient; +import com.anthropic.client.okhttp.AnthropicOkHttpClient; +import com.anthropic.models.messages.Message; +import com.anthropic.models.messages.MessageCreateParams; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; import org.springframework.stereotype.Component; -import org.springframework.web.reactive.function.client.WebClient; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.util.Base64; -import java.util.List; -import java.util.Map; import java.util.regex.Pattern; +import java.util.stream.Collectors; /** - * Direct REST client for LM Studio LLM API. Communicates via the - * OpenAI-compatible /v1/chat/completions endpoint. + * LLM client backed by the Anthropic Claude API (official Java SDK). Keeps the + * same public interface as the previous LM-Studio-based implementation so + * callers (AiStatisticsService, TranslationService) stay unchanged. */ @Component @Slf4j @@ -27,34 +23,14 @@ public class LlmRestClient { private static final Pattern THINK_BLOCK_PATTERN = Pattern.compile("(?is).*?"); private static final Pattern THINK_TAG_PATTERN = Pattern.compile("(?is)"); - private final WebClient webClient; - private final ObjectMapper objectMapper; + private final AnthropicClient client; private final String model; - public LlmRestClient(@Value("${app.ai.lmstudio.base-url}") String lmstudioBaseUrl, - @Value("${app.ai.lmstudio.model}") String lmstudioModel, - @Value("${app.ai.lmstudio.htaccess-username}") String lmstudioHtaccessUsername, - @Value("${app.ai.lmstudio.htaccess-password}") String lmstudioHtaccessPassword, ObjectMapper objectMapper) { - - this.model = lmstudioModel; - this.objectMapper = objectMapper; - - WebClient.Builder builder = WebClient.builder(); - builder.baseUrl(lmstudioBaseUrl + "/v1/chat/completions"); - - if (lmstudioHtaccessUsername != null && !lmstudioHtaccessUsername.isBlank() && lmstudioHtaccessPassword != null - && !lmstudioHtaccessPassword.isBlank()) { - String credentials = lmstudioHtaccessUsername + ":" + lmstudioHtaccessPassword; - String encoded = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8)); - builder.defaultHeader(HttpHeaders.AUTHORIZATION, "Basic " + encoded); - log.info("LlmRestClient initialized (with HTACCESS auth) - URL: {}/v1/chat/completions, Model: {}", - lmstudioBaseUrl, lmstudioModel); - } else { - log.info("LlmRestClient initialized - URL: {}/v1/chat/completions, Model: {}", lmstudioBaseUrl, - lmstudioModel); - } - - this.webClient = builder.build(); + public LlmRestClient(@Value("${app.ai.anthropic.api-key}") String apiKey, + @Value("${app.ai.anthropic.model:claude-opus-4-8}") String model) { + this.model = model; + this.client = AnthropicOkHttpClient.builder().apiKey(apiKey).build(); + log.info("LlmRestClient initialized - Provider: Anthropic Claude, Model: {}", model); } /** @@ -78,32 +54,45 @@ public class LlmRestClient { * @param userMessage * User message/question * @param temperature - * Temperature for response randomness (0.0-1.0) + * Ignored - current Claude models do not accept sampling + * parameters (kept for signature compatibility) * @param maxTokens * Maximum tokens in response * @return LLM response text, or null on error */ public String chat(String systemPrompt, String userMessage, double temperature, int maxTokens) { try { - Map request = Map.of("model", model, "messages", - List.of(Map.of("role", "system", "content", systemPrompt != null ? systemPrompt : ""), - Map.of("role", "user", "content", userMessage)), - "temperature", temperature, "max_tokens", maxTokens, "stream", false); + MessageCreateParams.Builder builder = MessageCreateParams.builder().model(model).maxTokens(maxTokens); + if (systemPrompt != null && !systemPrompt.isBlank()) { + builder.system(systemPrompt); + } + builder.addUserMessage(userMessage); - log.info("Sending request to LLM (model: {}, prompt length: {} chars)...", model, userMessage.length()); + log.info("Sending request to Claude (model: {}, prompt length: {} chars)...", model, userMessage.length()); long startTime = System.currentTimeMillis(); - String response = webClient.post().contentType(MediaType.APPLICATION_JSON).bodyValue(request).retrieve() - .bodyToMono(String.class).timeout(Duration.ofSeconds(120)).block(); + Message response = client.messages().create(builder.build()); long duration = System.currentTimeMillis() - startTime; - log.info("LLM response received in {}ms", duration); - log.debug("LLM response payload received ({} chars)", response != null ? response.length() : 0); + log.info("Claude response received in {}ms (stop_reason: {})", duration, response.stopReason()); - return extractContent(response); + String content = response.content().stream().flatMap(block -> block.text().stream()) + .map(textBlock -> textBlock.text()).collect(Collectors.joining("\n")); + + if (content.isBlank()) { + log.warn("Claude response content is empty"); + return null; + } + + String sanitizedContent = sanitizeAssistantContent(content); + if (sanitizedContent.isBlank()) { + log.warn("Claude response content is empty after sanitization"); + return null; + } + return sanitizedContent; } catch (Exception e) { - log.error("Error calling LLM API: {} - {}", e.getClass().getSimpleName(), e.getMessage()); + log.error("Error calling Claude API: {} - {}", e.getClass().getSimpleName(), e.getMessage()); if (log.isDebugEnabled()) { log.debug("Full stack trace:", e); } @@ -122,35 +111,6 @@ public class LlmRestClient { return model; } - private String extractContent(String response) { - if (response == null || response.isBlank()) { - log.warn("LLM returned null or blank response"); - return null; - } - try { - JsonNode root = objectMapper.readTree(response); - JsonNode choices = root.path("choices"); - if (choices.isArray() && !choices.isEmpty()) { - String content = choices.get(0).path("message").path("content").asText(); - if (content == null || content.isBlank()) { - log.warn("LLM response content is empty"); - return null; - } - String sanitizedContent = sanitizeAssistantContent(content); - if (sanitizedContent.isBlank()) { - log.warn("LLM response content is empty after sanitization"); - return null; - } - return sanitizedContent; - } - log.warn("Unexpected response structure (no choices): {}", response); - return null; - } catch (Exception e) { - log.error("Error parsing LLM response: {}", e.getMessage()); - return null; - } - } - private String sanitizeAssistantContent(String content) { String sanitized = THINK_BLOCK_PATTERN.matcher(content).replaceAll(" "); sanitized = THINK_TAG_PATTERN.matcher(sanitized).replaceAll(" "); diff --git a/backend/src/main/java/de/assecutor/votianlt/controller/MessageController.java b/backend/src/main/java/de/assecutor/votianlt/controller/MessageController.java index e503e56..2c2b599 100644 --- a/backend/src/main/java/de/assecutor/votianlt/controller/MessageController.java +++ b/backend/src/main/java/de/assecutor/votianlt/controller/MessageController.java @@ -128,8 +128,10 @@ public class MessageController { log.info("[JOBS] Retrieving assigned jobs for appUserId: {}", appUserId); - List assignedJobs = jobRepository.findByAppUser(appUserId); - log.info("[JOBS] Found {} jobs for appUserId: {}", assignedJobs.size(), appUserId); + List assignedJobs = jobRepository.findByAppUser(appUserId).stream() + .filter(job -> job.getStatus() != JobStatus.COMPLETED && job.getStatus() != JobStatus.CANCELLED) + .toList(); + log.info("[JOBS] Found {} open jobs for appUserId: {}", assignedJobs.size(), appUserId); List jobsWithRelatedData = assignedJobs.stream().map(job -> { List cargoItems = cargoItemRepository.findByJobId(job.getId()); @@ -462,15 +464,19 @@ public class MessageController { */ public void handleStationCompleted(String appUserId, Map payload) { try { + String jobIdStr = payload.get("jobId") != null ? payload.get("jobId").toString() : null; String jobNumber = payload.get("jobNumber") != null ? payload.get("jobNumber").toString() : null; - if (jobNumber == null || jobNumber.isBlank()) { - log.warn("[STATION] station_completed without jobNumber"); - return; - } - Optional jobOpt = jobRepository.findByJobNumber(jobNumber); + Optional jobOpt = Optional.empty(); + if (jobIdStr != null && ObjectId.isValid(jobIdStr)) { + jobOpt = jobRepository.findById(new ObjectId(jobIdStr)); + } + if (jobOpt.isEmpty() && jobNumber != null && !jobNumber.isBlank()) { + jobOpt = jobRepository.findByJobNumber(jobNumber); + } if (jobOpt.isEmpty()) { - log.warn("[STATION] Job with jobNumber {} not found", jobNumber); + log.warn("[STATION] Job not found for station_completed (jobId={}, jobNumber={})", jobIdStr, + jobNumber); return; } diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/view/InvoiceGeneratorView.java b/backend/src/main/java/de/assecutor/votianlt/pages/view/InvoiceGeneratorView.java index ab349fa..cf0bf5e 100644 --- a/backend/src/main/java/de/assecutor/votianlt/pages/view/InvoiceGeneratorView.java +++ b/backend/src/main/java/de/assecutor/votianlt/pages/view/InvoiceGeneratorView.java @@ -5,7 +5,10 @@ import com.vaadin.flow.component.ClientCallable; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.dependency.JsModule; +import com.vaadin.flow.component.dialog.Dialog; import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.IFrame; +import com.vaadin.flow.component.html.Input; import com.vaadin.flow.component.html.Span; import com.vaadin.flow.component.icon.Icon; import com.vaadin.flow.component.icon.VaadinIcon; @@ -13,32 +16,56 @@ 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.textfield.TextField; -import com.vaadin.flow.component.html.Input; -import com.vaadin.flow.component.dialog.Dialog; -import com.vaadin.flow.component.html.IFrame; -import elemental.json.JsonValue; -import elemental.json.JsonType; import com.vaadin.flow.component.upload.Upload; import com.vaadin.flow.component.upload.receivers.MemoryBuffer; import com.vaadin.flow.router.HasDynamicTitle; import com.vaadin.flow.router.Route; +import de.assecutor.votianlt.model.invoices.SystemInvoiceData; import de.assecutor.votianlt.pages.base.ui.component.DialogStylingHelper; import de.assecutor.votianlt.pages.base.ui.view.AdminLayout; import de.assecutor.votianlt.service.CustomerInvoiceService; +import de.assecutor.votianlt.service.InvoiceTemplateService; import jakarta.annotation.security.RolesAllowed; +import lombok.extern.slf4j.Slf4j; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Base64; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; + +/** + * Admin canvas designer for the SYSTEM invoice template: the template used for + * invoices from Assecutor Data Service GmbH to the users of this system. + * Mirrors the per-user invoice generator on the profile page (customer + * invoices), but the issuer master data is fixed to Assecutor and the + * recipient variables are filled with the invoiced user's data. + */ @Route(value = "invoice-generator", layout = AdminLayout.class) @RolesAllowed("ADMIN") -@JsModule("./invoice-generator/invoice-generator.js") +@JsModule("./invoice-generator/profile-invoice-generator.js") +@Slf4j public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTitle { - private final CustomerInvoiceService customerInvoiceService; - private Div canvasContainer; - private VerticalLayout propertiesPanel; - private Div selectedElementInfo; + /** + * Reserved userId under which the system invoice template is stored in the + * invoice_templates collection. + */ + public static final String SYSTEM_TEMPLATE_USER_ID = "SYSTEM"; - public InvoiceGeneratorView(CustomerInvoiceService customerInvoiceService) { + private static final BigDecimal SYSTEM_VAT_RATE = new BigDecimal("0.19"); + + private final CustomerInvoiceService customerInvoiceService; + private final InvoiceTemplateService invoiceTemplateService; + + private VerticalLayout propertiesPanel; + + public InvoiceGeneratorView(CustomerInvoiceService customerInvoiceService, + InvoiceTemplateService invoiceTemplateService) { this.customerInvoiceService = customerInvoiceService; + this.invoiceTemplateService = invoiceTemplateService; setId("invoice-generator-view"); addClassNames("invoice-generator-view", "admin-form-view"); @@ -51,27 +78,24 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi getStyle().set("overflow", "hidden").set("box-sizing", "border-box").set("display", "flex") .set("flex-direction", "column"); - // Hauptlayout: Links (Templates) | Mitte (Canvas) | Rechts (Eigenschaften) + // Hauptlayout: Links (Bausteine) | Mitte (Canvas) | Rechts (Eigenschaften) HorizontalLayout mainLayout = new HorizontalLayout(); mainLayout.setWidth("100%"); - mainLayout.setHeight("calc(100vh - 60px)"); // Abzug für Action-Buttons + mainLayout.setHeight("calc(100vh - 60px)"); mainLayout.setSpacing(true); mainLayout.getStyle().set("overflow", "hidden"); mainLayout.addClassName("invoice-generator-main"); - // Linke Seite: Templates/Bausteine VerticalLayout leftPanel = createTemplatesPanel(); - leftPanel.setWidth("250px"); + leftPanel.setWidth("280px"); leftPanel.setHeightFull(); - leftPanel.getStyle().set("flex-shrink", "0").set("min-width", "250px").set("overflow", "auto"); + leftPanel.getStyle().set("flex-shrink", "0").set("min-width", "280px").set("overflow", "auto"); - // Mitte: Canvas mit Konva.js VerticalLayout centerPanel = createCanvasPanel(); centerPanel.setWidth("60%"); centerPanel.setHeightFull(); centerPanel.getStyle().set("flex-grow", "1").set("min-width", "0"); - // Rechte Seite: Eigenschaften propertiesPanel = createPropertiesPanel(); propertiesPanel.setWidth("300px"); propertiesPanel.setHeightFull(); @@ -82,7 +106,6 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi add(mainLayout); - // Aktions-Buttons unter dem Canvas (fixe Höhe) HorizontalLayout actionLayout = createActionButtons(); actionLayout.setHeight("60px"); actionLayout.getStyle().set("flex-shrink", "0"); @@ -92,12 +115,97 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi @Override protected void onAttach(AttachEvent attachEvent) { super.onAttach(attachEvent); - // Register this view instance and initialize the canvas - getElement().executeJs("window.invoiceGeneratorView = this;" + "if (window.invoiceGenerator) {" - + " console.log('Initializing invoice generator...');" + " window.invoiceGenerator.init();" - + "} else {" + " console.error('Invoice generator not found');" + "}"); + String masterdataJson = buildMasterdataJson(); + getElement().executeJs("window.invoiceGeneratorViewProfile = $0;" + + "window.profileInvoiceVatRate = " + SYSTEM_VAT_RATE.doubleValue() + ";" + + "window.masterdataValues = JSON.parse($1);" + + "setTimeout(function() { " + + " if (window.initProfileInvoiceGenerator) { " + + " window.initProfileInvoiceGenerator(); " + + " setTimeout(function() { $0.$server.onCanvasReady(); }, 200); " + + " } else { console.error('initProfileInvoiceGenerator not found'); } " + + "}, 300);", getElement(), masterdataJson); } + /** + * Called by JavaScript when the canvas is ready. + */ + @ClientCallable + public void onCanvasReady() { + loadSystemTemplate(); + } + + private void loadSystemTemplate() { + try { + Optional optionalTemplate = invoiceTemplateService + .getTemplateByUserId(SYSTEM_TEMPLATE_USER_ID); + if (optionalTemplate.isEmpty()) { + return; + } + String templateData = optionalTemplate.get().getTemplateData(); + if (templateData == null || templateData.isEmpty()) { + return; + } + getElement().executeJs("setTimeout(function() { " + + " if (window.loadProfileTemplate && document.getElementById('invoice-canvas-container-profile')) { " + + " window.loadProfileTemplate(JSON.parse($0)); " + + " } else { console.error('loadProfileTemplate or canvas not available'); } " + + "}, 300);", templateData); + } catch (Exception ex) { + log.error("Error loading system invoice template: {}", ex.getMessage(), ex); + } + } + + /** + * Build the variable substitution values for the system invoice template. + * Issuer data comes from SystemInvoiceData (Assecutor Data Service GmbH), + * recipient values are sample data replaced with the actual user when a real + * invoice is generated. + */ + private Map buildSystemVariables() { + SystemInvoiceData data = new SystemInvoiceData(); + Map variables = new LinkedHashMap<>(); + String companyFull = (safe(data.getCompanyName()) + " " + safe(data.getCompanySubtitle())).trim(); + variables.put("masterdata.company_name", companyFull); + variables.put("masterdata.street", safe(data.getCompanyStreet())); + variables.put("masterdata.city", safe(data.getCompanyCity())); + variables.put("masterdata.phone", safe(data.getCompanyPhone())); + variables.put("masterdata.email", safe(data.getCompanyEmail())); + variables.put("masterdata.website", safe(data.getCompanyWebsite())); + variables.put("masterdata.sender_line", safe(data.getSenderLine())); + variables.put("masterdata.payment_terms", safe(data.getPaymentTerms())); + variables.put("masterdata.footer", safe(data.getFooterText()).replace("
", "\n")); + variables.put("masterdata.invoice_number", "SYS-2026-0001"); + variables.put("masterdata.invoice_date", + LocalDate.now().format(DateTimeFormatter.ofPattern("dd.MM.yyyy"))); + + // Empfänger (Nutzer des Systems) - Beispieldaten für Entwurf und Vorschau + variables.put("customer.company_name", "Musterfirma GmbH"); + variables.put("customer.contact_name", "Max Mustermann"); + variables.put("customer.street", "Musterstraße 1"); + variables.put("customer.city", "12345 Musterstadt"); + variables.put("customer.email", "nutzer@beispiel.de"); + variables.put("customer.phone", "0123 456789"); + return variables; + } + + private String buildMasterdataJson() { + try { + return new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(buildSystemVariables()); + } catch (Exception ex) { + log.error("Error serializing masterdata values: {}", ex.getMessage(), ex); + return "{}"; + } + } + + private String safe(String value) { + return value != null ? value : ""; + } + + // ========================================== + // Linkes Panel: Bausteine + // ========================================== + private VerticalLayout createTemplatesPanel() { VerticalLayout panel = new VerticalLayout(); panel.setPadding(true); @@ -106,96 +214,253 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi panel.getStyle().set("overflow", "auto"); panel.addClassName("invoice-generator-panel"); - Span header = new Span(getTranslation("invoicegenerator.panel.templates")); - header.addClassName("invoice-generator-panel-title"); + Map values = buildSystemVariables(); - // Draggable Templates - Div textBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.text"), VaadinIcon.TEXT_LABEL, + // Bereich 1: Rechnungssteller (Assecutor Data Service GmbH) + Span issuerHeader = createSectionHeader(getTranslation("invoicegenerator.issuer.header")); + + Div issuerCompany = createVariableTemplate(getTranslation("profile.company"), VaadinIcon.OFFICE, + "masterdata.company_name", values.get("masterdata.company_name")); + Div issuerStreet = createVariableTemplate(getTranslation("profile.street"), VaadinIcon.MAP_MARKER, + "masterdata.street", values.get("masterdata.street")); + Div issuerCity = createVariableTemplate(getTranslation("profile.invoice.city"), VaadinIcon.BUILDING, + "masterdata.city", values.get("masterdata.city")); + Div issuerPhone = createVariableTemplate(getTranslation("profile.invoice.phone"), VaadinIcon.PHONE, + "masterdata.phone", values.get("masterdata.phone")); + Div issuerEmail = createVariableTemplate(getTranslation("profile.invoice.email"), VaadinIcon.ENVELOPE, + "masterdata.email", values.get("masterdata.email")); + Div issuerWebsite = createVariableTemplate(getTranslation("invoicegenerator.issuer.website"), + VaadinIcon.GLOBE, "masterdata.website", values.get("masterdata.website")); + Div issuerSenderLine = createVariableTemplate(getTranslation("invoicegenerator.issuer.senderline"), + VaadinIcon.ENVELOPE_OPEN_O, "masterdata.sender_line", values.get("masterdata.sender_line")); + Div issuerPaymentTerms = createVariableTemplate(getTranslation("invoicegenerator.issuer.paymentterms"), + VaadinIcon.CREDIT_CARD, "masterdata.payment_terms", values.get("masterdata.payment_terms")); + Div issuerFooter = createVariableTemplate(getTranslation("invoicegenerator.issuer.footer"), + VaadinIcon.FILE_TEXT, "masterdata.footer", values.get("masterdata.footer")); + Div invoiceNumber = createVariableTemplate(getTranslation("profile.invoice.element.invoicenumber"), + VaadinIcon.HASH, "masterdata.invoice_number", values.get("masterdata.invoice_number")); + Div invoiceDate = createVariableTemplate(getTranslation("invoicegenerator.issuer.invoicedate"), + VaadinIcon.CALENDAR_CLOCK, "masterdata.invoice_date", values.get("masterdata.invoice_date")); + + // Bereich 2: Leistungen / Positionen + Span servicesHeader = createSectionHeader(getTranslation("profile.services.label")); + + Div servicesListBlock = createServicesVariableTemplate(getTranslation("profile.invoice.services.list"), + VaadinIcon.LIST, "services.list", "Position 1: 100,00 €\nPosition 2: 50,00 €"); + Div servicesNetBlock = createServicesVariableTemplate(getTranslation("profile.invoice.net"), + VaadinIcon.COIN_PILES, "services.net_total", "150,00 €"); + Div servicesGrossBlock = createServicesVariableTemplate(getTranslation("profile.invoice.gross"), + VaadinIcon.MONEY, "services.gross_total", "178,50 €"); + + // Bereich 3: Empfänger (Nutzer des Systems) + Span recipientHeader = createSectionHeader(getTranslation("invoicegenerator.recipient.header")); + + Div recipientCompany = createCustomerVariableTemplate(getTranslation("invoicegenerator.recipient.company"), + VaadinIcon.OFFICE, "customer.company_name", values.get("customer.company_name")); + Div recipientName = createCustomerVariableTemplate(getTranslation("invoicegenerator.recipient.name"), + VaadinIcon.USER, "customer.contact_name", values.get("customer.contact_name")); + Div recipientStreet = createCustomerVariableTemplate(getTranslation("invoicegenerator.recipient.street"), + VaadinIcon.MAP_MARKER, "customer.street", values.get("customer.street")); + Div recipientCity = createCustomerVariableTemplate(getTranslation("invoicegenerator.recipient.city"), + VaadinIcon.BUILDING, "customer.city", values.get("customer.city")); + Div recipientEmail = createCustomerVariableTemplate(getTranslation("invoicegenerator.recipient.email"), + VaadinIcon.ENVELOPE, "customer.email", values.get("customer.email")); + + // Bereich 4: Freie Elemente + Span freeHeader = createSectionHeader(getTranslation("profile.invoice.free.elements")); + + Div textBlock = createDraggableTemplate(getTranslation("profile.invoice.element.text"), VaadinIcon.TEXT_LABEL, "text"); - Div headerBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.header"), VaadinIcon.HEADER, + Div headerBlock = createDraggableTemplate(getTranslation("profile.invoice.element.header"), VaadinIcon.HEADER, "header"); - Div dateBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.date"), VaadinIcon.CALENDAR, + Div dateBlock = createDraggableTemplate(getTranslation("profile.invoice.element.date"), VaadinIcon.CALENDAR, "date"); - Div customerBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.customerinfo"), - VaadinIcon.USER, "customer"); - Div companyBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.companyinfo"), - VaadinIcon.OFFICE, "company"); - Div amountBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.amount"), - VaadinIcon.COIN_PILES, "amount"); - Div lineBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.line"), VaadinIcon.LINE_V, + Div lineBlock = createDraggableTemplate(getTranslation("profile.invoice.element.line"), VaadinIcon.LINE_V, "line"); - Div vlineBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.vline"), - VaadinIcon.LINE_BAR_CHART, "vline"); - Div imageBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.image"), VaadinIcon.PICTURE, + Div imageBlock = createDraggableTemplate(getTranslation("profile.invoice.element.image"), VaadinIcon.PICTURE, "image"); - panel.add(header, textBlock, headerBlock, dateBlock, customerBlock, companyBlock, amountBlock, lineBlock, - vlineBlock, imageBlock); + panel.add(issuerHeader, issuerCompany, issuerStreet, issuerCity, issuerPhone, issuerEmail, issuerWebsite, + issuerSenderLine, issuerPaymentTerms, issuerFooter, invoiceNumber, invoiceDate, servicesHeader, + servicesListBlock, servicesNetBlock, servicesGrossBlock, recipientHeader, recipientCompany, + recipientName, recipientStreet, recipientCity, recipientEmail, freeHeader, textBlock, headerBlock, + dateBlock, lineBlock, imageBlock); return panel; } - private Div createDraggableTemplate(String label, VaadinIcon icon, String type) { + private Span createSectionHeader(String text) { + Span header = new Span(text); + header.getStyle().set("font-weight", "bold").set("font-size", "var(--lumo-font-size-m)").set("margin-top", + "var(--lumo-space-s)"); + return header; + } + + /** + * Blauer Baustein: Stammdaten des Rechnungsstellers (masterdata.*). + */ + private Div createVariableTemplate(String label, VaadinIcon icon, String variable, String defaultText) { Div template = new Div(); template.setText(label); - template.addClassName("invoice-generator-template"); + template.getStyle().set("padding", "var(--lumo-space-s)").set("margin", "var(--lumo-space-xs) 0") + .set("background-color", "rgba(25, 118, 210, 0.1)").set("border", "1px solid rgba(25, 118, 210, 0.3)") + .set("border-radius", "var(--lumo-border-radius-m)").set("cursor", "grab").set("display", "flex") + .set("align-items", "center").set("gap", "var(--lumo-space-s)").set("user-select", "none") + .set("font-size", "var(--lumo-font-size-s)").set("color", "#1976d2"); - // Icon hinzufügen Icon templateIcon = icon.create(); templateIcon.setSize("var(--lumo-icon-size-s)"); template.getElement().insertChild(0, templateIcon.getElement()); - // Drag & Drop Attribute setzen template.getElement().setAttribute("draggable", "true"); - template.getElement().setAttribute("data-template-type", type); + template.getElement().setAttribute("data-template-type", "variable"); template.getElement().setAttribute("data-template-label", label); + template.getElement().setAttribute("data-variable", variable); + template.getElement().setAttribute("data-static-text", defaultText != null ? defaultText : ""); - // JavaScript Event Listener für Drag Start template.getElement() .executeJs("this.addEventListener('dragstart', function(e) {" + " e.dataTransfer.setData('template-type', this.getAttribute('data-template-type'));" + " e.dataTransfer.setData('template-label', this.getAttribute('data-template-label'));" + + " e.dataTransfer.setData('variable', this.getAttribute('data-variable'));" + + " e.dataTransfer.setData('static-text', this.getAttribute('data-static-text'));" + + " e.dataTransfer.setData('is-static', 'true');" + " this.style.opacity = '0.5';" + "});" + + "this.addEventListener('dragend', function(e) {" + " this.style.opacity = '1';" + "});"); + + return template; + } + + /** + * Grüner Baustein: Empfängerdaten (customer.* = Nutzer des Systems). + */ + private Div createCustomerVariableTemplate(String label, VaadinIcon icon, String variable, String defaultText) { + Div template = new Div(); + template.setText(label); + template.getStyle().set("padding", "var(--lumo-space-s)").set("margin", "var(--lumo-space-xs) 0") + .set("background-color", "rgba(46, 204, 113, 0.15)") + .set("border", "1px solid rgba(46, 204, 113, 0.4)").set("border-radius", "var(--lumo-border-radius-m)") + .set("cursor", "grab").set("display", "flex").set("align-items", "center") + .set("gap", "var(--lumo-space-s)").set("user-select", "none") + .set("font-size", "var(--lumo-font-size-s)").set("color", "#27ae60"); + + Icon templateIcon = icon.create(); + templateIcon.setSize("var(--lumo-icon-size-s)"); + template.getElement().insertChild(0, templateIcon.getElement()); + + template.getElement().setAttribute("draggable", "true"); + template.getElement().setAttribute("data-template-type", "variable"); + template.getElement().setAttribute("data-template-label", label); + template.getElement().setAttribute("data-variable", variable); + template.getElement().setAttribute("data-static-text", defaultText != null ? defaultText : ""); + template.getElement().setAttribute("data-is-customer", "true"); + + template.getElement() + .executeJs("this.addEventListener('dragstart', function(e) {" + + " e.dataTransfer.setData('template-type', this.getAttribute('data-template-type'));" + + " e.dataTransfer.setData('template-label', this.getAttribute('data-template-label'));" + + " e.dataTransfer.setData('variable', this.getAttribute('data-variable'));" + + " e.dataTransfer.setData('static-text', this.getAttribute('data-static-text'));" + + " e.dataTransfer.setData('is-static', 'true');" + + " e.dataTransfer.setData('is-customer', this.getAttribute('data-is-customer'));" + " this.style.opacity = '0.5';" + "});" + "this.addEventListener('dragend', function(e) {" + " this.style.opacity = '1';" + "});"); return template; } + /** + * Gelber Baustein: Leistungen/Positionen (services.*). + */ + private Div createServicesVariableTemplate(String label, VaadinIcon icon, String variable, String defaultText) { + Div template = new Div(); + template.setText(label); + template.getStyle().set("padding", "var(--lumo-space-s)").set("margin", "var(--lumo-space-xs) 0") + .set("background-color", "rgba(255, 193, 7, 0.15)") + .set("border", "1px solid rgba(255, 193, 7, 0.4)").set("border-radius", "var(--lumo-border-radius-m)") + .set("cursor", "grab").set("display", "flex").set("align-items", "center") + .set("gap", "var(--lumo-space-s)").set("user-select", "none") + .set("font-size", "var(--lumo-font-size-s)").set("color", "#f57c00"); + + Icon templateIcon = icon.create(); + templateIcon.setSize("var(--lumo-icon-size-s)"); + template.getElement().insertChild(0, templateIcon.getElement()); + + template.getElement().setAttribute("draggable", "true"); + template.getElement().setAttribute("data-template-type", "variable"); + template.getElement().setAttribute("data-template-label", label); + template.getElement().setAttribute("data-variable", variable); + template.getElement().setAttribute("data-static-text", defaultText != null ? defaultText : ""); + template.getElement().setAttribute("data-is-customer", "false"); + template.getElement().setAttribute("data-is-services", "true"); + + template.getElement() + .executeJs("this.addEventListener('dragstart', function(e) {" + + " e.dataTransfer.setData('template-type', this.getAttribute('data-template-type'));" + + " e.dataTransfer.setData('template-label', this.getAttribute('data-template-label'));" + + " e.dataTransfer.setData('variable', this.getAttribute('data-variable'));" + + " e.dataTransfer.setData('static-text', this.getAttribute('data-static-text'));" + + " e.dataTransfer.setData('is-static', 'true');" + + " e.dataTransfer.setData('is-customer', this.getAttribute('data-is-customer'));" + + " e.dataTransfer.setData('is-services', this.getAttribute('data-is-services'));" + + " this.style.opacity = '0.5';" + "});" + "this.addEventListener('dragend', function(e) {" + + " this.style.opacity = '1';" + "});"); + + return template; + } + + /** + * Neutraler Baustein: freie Elemente (Text, Überschrift, Datum, Linie, Bild). + */ + private Div createDraggableTemplate(String label, VaadinIcon icon, String type) { + Div template = new Div(); + template.setText(label); + template.addClassName("invoice-generator-template"); + + Icon templateIcon = icon.create(); + templateIcon.setSize("var(--lumo-icon-size-s)"); + template.getElement().insertChild(0, templateIcon.getElement()); + + template.getElement().setAttribute("draggable", "true"); + template.getElement().setAttribute("data-template-type", type); + template.getElement().setAttribute("data-template-label", label); + + template.getElement() + .executeJs("this.addEventListener('dragstart', function(e) {" + + " e.dataTransfer.setData('template-type', this.getAttribute('data-template-type'));" + + " e.dataTransfer.setData('template-label', this.getAttribute('data-template-label'));" + + " e.dataTransfer.setData('is-static', 'false');" + " this.style.opacity = '0.5';" + "});" + + "this.addEventListener('dragend', function(e) {" + " this.style.opacity = '1';" + "});"); + + return template; + } + + // ========================================== + // Mitte: Canvas + // ========================================== + private VerticalLayout createCanvasPanel() { VerticalLayout panel = new VerticalLayout(); panel.setPadding(false); panel.setSpacing(false); panel.setHeightFull(); - // Canvas Container für Konva.js - canvasContainer = new Div(); - canvasContainer.setId("invoice-canvas-container"); + // Container-ID muss zu profile-invoice-generator.js passen + Div canvasContainer = new Div(); + canvasContainer.setId("invoice-canvas-container-profile"); canvasContainer.setWidth("100%"); canvasContainer.setHeight("100%"); canvasContainer.addClassName("invoice-generator-canvas"); - // Drop Zone Event Listener - canvasContainer.getElement() - .executeJs("var container = this;" + "container.addEventListener('dragover', function(e) {" - + " e.preventDefault();" + " e.dataTransfer.dropEffect = 'copy';" - + " container.style.borderColor = 'var(--lumo-primary-color)';" + "});" - + "container.addEventListener('dragleave', function(e) {" - + " container.style.borderColor = 'var(--lumo-contrast-30pct)';" + "});" - + "container.addEventListener('drop', function(e) {" + " e.preventDefault();" - + " container.style.borderColor = 'var(--lumo-contrast-30pct)';" - + " var templateType = e.dataTransfer.getData('template-type');" - + " var templateLabel = e.dataTransfer.getData('template-label');" - + " if (templateType && window.invoiceGenerator) {" - + " var rect = container.getBoundingClientRect();" + " var x = e.clientX - rect.left;" - + " var y = e.clientY - rect.top;" - + " window.invoiceGenerator.addElement(templateType, templateLabel, x, y);" + " }" + "});"); - panel.add(canvasContainer); panel.expand(canvasContainer); return panel; } + // ========================================== + // Rechtes Panel: Eigenschaften + // ========================================== + private VerticalLayout createPropertiesPanel() { VerticalLayout panel = new VerticalLayout(); panel.setPadding(true); @@ -204,19 +469,248 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi panel.getStyle().set("overflow", "auto"); panel.addClassName("invoice-generator-panel"); - Span header = new Span(getTranslation("invoicegenerator.panel.properties")); + Span header = new Span(getTranslation("invoicegenerator.properties.title")); header.addClassName("invoice-generator-panel-title"); - // Info-Text wenn kein Element ausgewählt - selectedElementInfo = new Div(); - selectedElementInfo.setText(getTranslation("invoicegenerator.properties.info")); - selectedElementInfo.addClassName("invoice-generator-info"); + Div info = new Div(); + info.setText(getTranslation("invoicegenerator.properties.select.info")); + info.addClassName("invoice-generator-info"); - panel.add(header, selectedElementInfo); + panel.add(header, info); return panel; } + @ClientCallable + public void updatePropertiesPanel(String elementId, String elementType, String text, Double x, Double y, + Integer fontSize, String color, Double width, Double height, Boolean isStatic, String variable) { + getUI().ifPresent(ui -> ui.access(() -> { + propertiesPanel.removeAll(); + + Span header = new Span(getTranslation("invoicegenerator.properties.title")); + header.addClassName("invoice-generator-panel-title"); + + String typeDisplay = getTranslation("invoicegenerator.properties.type") + ": " + elementType; + if (variable != null && !variable.isEmpty()) { + typeDisplay += " (" + getTranslation("profile.invoice.variable") + ")"; + } + Span typeLabel = new Span(typeDisplay); + typeLabel.addClassName("invoice-generator-info"); + + propertiesPanel.add(header, typeLabel); + + // Variable anzeigen wenn vorhanden + if (variable != null && !variable.isEmpty()) { + TextField variableField = new TextField(getTranslation("profile.invoice.variable")); + variableField.setValue(variable); + variableField.setReadOnly(true); + variableField.setWidthFull(); + variableField.setHelperText(getVariableDescription(variable)); + propertiesPanel.add(variableField); + } + + // Für Bildelemente: Upload anzeigen + if ("image".equals(elementType)) { + MemoryBuffer buffer = new MemoryBuffer(); + Upload upload = new Upload(buffer); + upload.setAcceptedFileTypes("image/png", "image/jpeg", "image/jpg", "image/gif", "image/webp"); + upload.setMaxFileSize(5 * 1024 * 1024); // 5 MB + upload.setDropLabel(new Span(getTranslation("invoicegenerator.upload.drop"))); + upload.setWidthFull(); + + upload.addSucceededListener(event -> { + try { + byte[] bytes = buffer.getInputStream().readAllBytes(); + String base64 = Base64.getEncoder().encodeToString(bytes); + String mimeType = event.getMIMEType(); + String dataUrl = "data:" + mimeType + ";base64," + base64; + + getElement() + .executeJs("if (window.updateProfileElementImage) { window.updateProfileElementImage('" + + elementId + "', $0); }", dataUrl); + showNotification(getTranslation("invoicegenerator.upload.success")); + } catch (Exception ex) { + showNotification(getTranslation("invoicegenerator.upload.error", ex.getMessage())); + } + }); + + upload.addFileRejectedListener(event -> { + showNotification(getTranslation("invoicegenerator.file.rejected", event.getErrorMessage())); + }); + + propertiesPanel.add(upload); + } + + // Text Feld (nur für Text-Elemente) + if (!"line".equals(elementType) && !"image".equals(elementType)) { + TextField textField = new TextField(getTranslation("profile.invoice.element.text")); + textField.setValue(text != null ? text : ""); + textField.setWidthFull(); + if (Boolean.TRUE.equals(isStatic)) { + textField.setReadOnly(true); + textField.setHelperText(getTranslation("profile.invoice.text.from.masterdata")); + } else { + textField.addValueChangeListener(e -> { + getElement() + .executeJs("if (window.updateProfileElementText) { window.updateProfileElementText('" + + elementId + "', $0); }", e.getValue()); + }); + } + propertiesPanel.add(textField); + } + + // X Position + TextField xField = new TextField("X Position"); + xField.setValue(x != null ? String.valueOf(Math.round(x)) : "0"); + xField.setWidthFull(); + xField.addValueChangeListener(e -> { + try { + double newX = Double.parseDouble(e.getValue()); + getElement().executeJs( + "if (window.updateProfileElementPosition) { window.updateProfileElementPosition('" + + elementId + "', $0, null); }", + newX); + } catch (NumberFormatException ignored) { + } + }); + propertiesPanel.add(xField); + + // Y Position + TextField yField = new TextField("Y Position"); + yField.setValue(y != null ? String.valueOf(Math.round(y)) : "0"); + yField.setWidthFull(); + yField.addValueChangeListener(e -> { + try { + double newY = Double.parseDouble(e.getValue()); + getElement().executeJs( + "if (window.updateProfileElementPosition) { window.updateProfileElementPosition('" + + elementId + "', null, $0); }", + newY); + } catch (NumberFormatException ignored) { + } + }); + propertiesPanel.add(yField); + + // Schriftgröße und Farbe (nur für Text-Elemente) + if (!"line".equals(elementType) && !"image".equals(elementType)) { + TextField fontSizeField = new TextField(getTranslation("invoicegenerator.fontsize.label")); + fontSizeField.setValue(fontSize != null ? String.valueOf(fontSize) : "16"); + fontSizeField.setWidthFull(); + fontSizeField.addValueChangeListener(e -> { + try { + int newSize = Integer.parseInt(e.getValue()); + getElement().executeJs( + "if (window.updateProfileElementFontSize) { window.updateProfileElementFontSize('" + + elementId + "', $0); }", + newSize); + } catch (NumberFormatException ignored) { + } + }); + propertiesPanel.add(fontSizeField); + + // Farbe + Div colorContainer = new Div(); + colorContainer.getStyle().set("display", "flex").set("align-items", "center") + .set("gap", "var(--lumo-space-s)").set("margin-top", "var(--lumo-space-s)"); + + Span colorLabel = new Span(getTranslation("invoicegenerator.color.label")); + colorLabel.getStyle().set("font-size", "var(--lumo-font-size-s)"); + + Input colorPicker = new Input(); + colorPicker.setType("color"); + colorPicker.setValue(color != null ? color : "#333333"); + colorPicker.getStyle().set("width", "50px").set("height", "32px").set("padding", "0") + .set("border", "1px solid var(--lumo-contrast-20pct)") + .set("border-radius", "var(--lumo-border-radius-m)").set("cursor", "pointer"); + + TextField colorHexField = new TextField(); + colorHexField.setValue(color != null ? color : "#333333"); + colorHexField.setWidth("100px"); + colorHexField.getStyle().set("margin", "0"); + + colorPicker.addValueChangeListener(e -> { + String newColor = e.getValue(); + colorHexField.setValue(newColor); + getElement().executeJs("if (window.updateProfileElementColor) { window.updateProfileElementColor('" + + elementId + "', $0); }", newColor); + }); + + colorHexField.addValueChangeListener(e -> { + String newColor = e.getValue(); + if (newColor.matches("^#[0-9A-Fa-f]{6}$")) { + colorPicker.setValue(newColor); + getElement() + .executeJs("if (window.updateProfileElementColor) { window.updateProfileElementColor('" + + elementId + "', $0); }", newColor); + } + }); + + colorContainer.add(colorLabel, colorPicker, colorHexField); + propertiesPanel.add(colorContainer); + } + + // Löschen Button + Button deleteButton = new Button(getTranslation("invoicegenerator.button.delete"), + new Icon(VaadinIcon.TRASH)); + deleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY); + deleteButton.setWidthFull(); + deleteButton.addClickListener(e -> { + getElement().executeJs( + "if (window.deleteProfileElement) { window.deleteProfileElement('" + elementId + "'); }"); + resetPropertiesPanel(); + }); + propertiesPanel.add(deleteButton); + })); + } + + @ClientCallable + public void resetPropertiesPanel() { + getUI().ifPresent(ui -> ui.access(() -> { + if (propertiesPanel == null) { + return; + } + propertiesPanel.removeAll(); + + Span header = new Span(getTranslation("invoicegenerator.properties.title")); + header.addClassName("invoice-generator-panel-title"); + + Div info = new Div(); + info.setText(getTranslation("invoicegenerator.properties.select.info")); + info.addClassName("invoice-generator-info"); + + propertiesPanel.add(header, info); + })); + } + + private String getVariableDescription(String variable) { + return switch (variable) { + case "masterdata.company_name" -> "Firmenname des Rechnungsstellers"; + case "masterdata.street" -> "Straße und Hausnummer des Rechnungsstellers"; + case "masterdata.city" -> "PLZ und Ort des Rechnungsstellers"; + case "masterdata.phone" -> "Telefonnummer des Rechnungsstellers"; + case "masterdata.email" -> "E-Mail-Adresse des Rechnungsstellers"; + case "masterdata.website" -> "Webseite des Rechnungsstellers"; + case "masterdata.sender_line" -> "Absenderzeile über dem Adressfeld"; + case "masterdata.payment_terms" -> "Zahlungsbedingungen"; + case "masterdata.footer" -> "Fußzeile mit Geschäftsführung, Registerdaten und Bankverbindung"; + case "masterdata.invoice_number" -> "Rechnungsnummer"; + case "masterdata.invoice_date" -> "Rechnungsdatum"; + case "customer.company_name" -> "Firmenname des Nutzers (Rechnungsempfänger)"; + case "customer.contact_name" -> "Name des Nutzers (Rechnungsempfänger)"; + case "customer.street" -> "Straße des Nutzers"; + case "customer.city" -> "PLZ und Ort des Nutzers"; + case "customer.email" -> "E-Mail des Nutzers"; + case "services.list" -> "Liste aller Rechnungspositionen"; + case "services.net_total" -> "Nettosumme aller Positionen"; + case "services.gross_total" -> "Bruttosumme aller Positionen"; + default -> "Variable: " + variable; + }; + } + + // ========================================== + // Aktionen + // ========================================== + private HorizontalLayout createActionButtons() { HorizontalLayout layout = new HorizontalLayout(); layout.setWidth("100%"); @@ -229,7 +723,7 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi Button clearButton = new Button(getTranslation("invoicegenerator.button.clear"), new Icon(VaadinIcon.TRASH)); clearButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY); clearButton.addClickListener(e -> { - getElement().executeJs("if (window.invoiceGenerator) { window.invoiceGenerator.clearCanvas(); }"); + getElement().executeJs("if (window.clearProfileCanvas) { window.clearProfileCanvas(); }"); showNotification(getTranslation("invoicegenerator.notification.cleared")); }); @@ -237,44 +731,42 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi previewButton.addThemeVariants(ButtonVariant.LUMO_CONTRAST); previewButton.addClickListener(e -> generatePreviewPdf()); - Button saveTemplateButton = new Button(getTranslation("button.savetemplate"), new Icon(VaadinIcon.DOWNLOAD)); + Button saveTemplateButton = new Button(getTranslation("button.savetemplate"), new Icon(VaadinIcon.CHECK)); saveTemplateButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - saveTemplateButton.addClickListener(e -> { - getElement().executeJs("if (window.invoiceGenerator) { window.invoiceGenerator.exportTemplate(); }"); - }); + saveTemplateButton.addClickListener(e -> saveSystemTemplate()); - Button generatePdfButton = new Button(getTranslation("invoicegenerator.button.generatepdf"), - new Icon(VaadinIcon.FILE_TEXT_O)); - generatePdfButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_SUCCESS); - generatePdfButton.addClickListener(e -> generatePdf()); - - layout.add(clearButton, previewButton, saveTemplateButton, generatePdfButton); + layout.add(clearButton, previewButton, saveTemplateButton); return layout; } - private void generatePdf() { - try { - // Hole die Canvas-Daten und generiere PDF - getElement().executeJs( - "if (window.invoiceGenerator) { return window.invoiceGenerator.getCanvasData(); } else { return null; }") - .then(json -> { - if (json == null) { - showNotification(getTranslation("invoicegenerator.notification.canvas.error")); - return; + private void saveSystemTemplate() { + getElement().executeJs( + "if (window.getProfileCanvasData) { return JSON.stringify(window.getProfileCanvasData()); } else { return null; }") + .then(result -> { + if (result == null) { + showNotification(getTranslation("invoicegenerator.notification.canvas.error")); + return; + } + try { + String templateData; + if (result instanceof elemental.json.JsonValue jsonValue) { + templateData = jsonValue.toJson(); + } else { + templateData = result.toString(); } - // Hier würde die PDF-Generierung erfolgen - showNotification(getTranslation("invoicegenerator.notification.generating")); - }); - } catch (Exception ex) { - showNotification(getTranslation("invoicegenerator.notification.pdf.error", ex.getMessage())); - } + invoiceTemplateService.saveTemplate(SYSTEM_TEMPLATE_USER_ID, templateData); + showNotification(getTranslation("invoicegenerator.template.saved")); + } catch (Exception ex) { + showNotification(getTranslation("invoicegenerator.template.save.error", ex.getMessage())); + } + }); } private void generatePreviewPdf() { try { getElement().executeJs( - "if (window.invoiceGenerator) { return window.invoiceGenerator.exportTemplateJson(); } else { return null; }") + "if (window.getProfileCanvasData) { return window.getProfileCanvasData(); } else { return null; }") .then(result -> { if (result == null) { showNotification(getTranslation("invoicegenerator.notification.canvas.error")); @@ -282,17 +774,13 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi } try { String templateData; - if (result instanceof JsonValue) { - JsonValue jsonValue = (JsonValue) result; - if (jsonValue.getType() == JsonType.STRING) { - templateData = jsonValue.asString(); - } else { - templateData = jsonValue.toJson(); - } + if (result instanceof elemental.json.JsonValue jsonValue) { + templateData = jsonValue.toJson(); } else { templateData = result.toString(); } - byte[] pdfBytes = customerInvoiceService.generatePdfFromCanvasTemplate(templateData); + byte[] pdfBytes = customerInvoiceService.generatePdfFromCanvasTemplate(templateData, + buildSystemVariables(), SYSTEM_VAT_RATE); showPdfInDialog(pdfBytes); } catch (Exception ex) { showNotification( @@ -300,7 +788,7 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi } }); } catch (Exception ex) { - showNotification(getTranslation("invoicegenerator.notification.error", ex.getMessage())); + showNotification(getTranslation("invoicegenerator.notification.preview.error", ex.getMessage())); } } @@ -309,14 +797,12 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi "90vw"); pdfDialog.setHeight("90vh"); - // Create a Div to hold the PDF viewer Div pdfContainer = new Div(); pdfContainer.setWidth("100%"); pdfContainer.setHeight("100%"); pdfContainer.addClassName("invoice-generator-pdf"); - // Use an iframe with data URL for PDF display - String base64Pdf = java.util.Base64.getEncoder().encodeToString(pdfBytes); + String base64Pdf = Base64.getEncoder().encodeToString(pdfBytes); String dataUrl = "data:application/pdf;base64," + base64Pdf; IFrame pdfFrame = new IFrame(); @@ -327,11 +813,9 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi pdfContainer.add(pdfFrame); - // Close button Button closeButton = new Button(getTranslation("button.close"), e -> pdfDialog.close()); closeButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - // Download button Button downloadButton = new Button(getTranslation("button.download"), e -> { getElement() .executeJs("const link = document.createElement('a');" + "link.href = 'data:application/pdf;base64," @@ -348,248 +832,6 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi Notification.show(message, 3000, Notification.Position.BOTTOM_CENTER); } - // Wird von JavaScript aufgerufen wenn ein Element ausgewählt wird - @ClientCallable - public void updatePropertiesPanel(String elementId, String elementType, String text, Double x, Double y, - Integer fontSize, String color) { - getUI().ifPresent(ui -> ui.access(() -> { - propertiesPanel.removeAll(); - - Span header = new Span(getTranslation("invoicegenerator.properties.title")); - header.addClassName("invoice-generator-panel-title"); - - // Element Typ Anzeige - Span typeLabel = new Span(getTranslation("invoicegenerator.properties.type") + ": " + elementType); - typeLabel.addClassName("invoice-generator-info"); - - propertiesPanel.add(header, typeLabel); - - // Für Bildelemente: Upload-Button anzeigen - if ("image".equals(elementType)) { - MemoryBuffer buffer = new MemoryBuffer(); - Upload upload = new Upload(buffer); - upload.setAcceptedFileTypes("image/png", "image/jpeg", "image/jpg", "image/gif", "image/webp"); - upload.setMaxFileSize(5 * 1024 * 1024); // 5 MB - upload.setDropLabel(new Span(getTranslation("invoicegenerator.upload.drop"))); - upload.setWidthFull(); - - upload.addSucceededListener(event -> { - try { - // Bild als Base64 kodieren - byte[] bytes = buffer.getInputStream().readAllBytes(); - String base64 = java.util.Base64.getEncoder().encodeToString(bytes); - String mimeType = event.getMIMEType(); - String dataUrl = "data:" + mimeType + ";base64," + base64; - - // An JavaScript übergeben - getElement() - .executeJs("if (window.invoiceGenerator) { window.invoiceGenerator.updateElementImage('" - + elementId + "', $0); }", dataUrl); - showNotification(getTranslation("invoicegenerator.upload.success")); - } catch (Exception ex) { - showNotification(getTranslation("invoicegenerator.upload.error", ex.getMessage())); - } - }); - - upload.addFileRejectedListener(event -> { - showNotification(getTranslation("invoicegenerator.file.rejected", event.getErrorMessage())); - }); - - propertiesPanel.add(upload); - } - - // Text Feld (nur für Text-Elemente) - if (!"line".equals(elementType) && !"vline".equals(elementType) && !"image".equals(elementType)) { - TextField textField = new TextField("Text"); - textField.setValue(text != null ? text : ""); - textField.setWidthFull(); - textField.addValueChangeListener(e -> { - getElement().executeJs("if (window.invoiceGenerator) { window.invoiceGenerator.updateElementText('" - + elementId + "', $0); }", e.getValue()); - }); - propertiesPanel.add(textField); - } - - // X Position - TextField xField = new TextField("X Position"); - xField.setValue(x != null ? String.valueOf(Math.round(x)) : "0"); - xField.setWidthFull(); - xField.addValueChangeListener(e -> { - try { - double newX = Double.parseDouble(e.getValue()); - getElement() - .executeJs("if (window.invoiceGenerator) { window.invoiceGenerator.updateElementPosition('" - + elementId + "', $0, null); }", newX); - } catch (NumberFormatException ignored) { - } - }); - propertiesPanel.add(xField); - - // Y Position - TextField yField = new TextField("Y Position"); - yField.setValue(y != null ? String.valueOf(Math.round(y)) : "0"); - yField.setWidthFull(); - yField.addValueChangeListener(e -> { - try { - double newY = Double.parseDouble(e.getValue()); - getElement() - .executeJs("if (window.invoiceGenerator) { window.invoiceGenerator.updateElementPosition('" - + elementId + "', null, $0); }", newY); - } catch (NumberFormatException ignored) { - } - }); - propertiesPanel.add(yField); - - // Font Size (nur für Text-Elemente) - if (!"line".equals(elementType) && !"vline".equals(elementType) && !"image".equals(elementType)) { - TextField fontSizeField = new TextField(getTranslation("invoicegenerator.fontsize.label")); - fontSizeField.setValue(fontSize != null ? String.valueOf(fontSize) : "16"); - fontSizeField.setWidthFull(); - fontSizeField.addValueChangeListener(e -> { - try { - int newSize = Integer.parseInt(e.getValue()); - getElement().executeJs( - "if (window.invoiceGenerator) { window.invoiceGenerator.updateElementFontSize('" - + elementId + "', $0); }", - newSize); - } catch (NumberFormatException ignored) { - } - }); - propertiesPanel.add(fontSizeField); - - // Schriftfarbe mit Dialog - Span colorLabel = new Span(getTranslation("invoicegenerator.color.label")); - colorLabel.getStyle().set("font-size", "var(--lumo-font-size-s)"); - propertiesPanel.add(colorLabel); - - // Aktuelle Farbe anzeigen und klickbar machen - String currentColor = color != null ? color : "#333333"; - - HorizontalLayout colorPreviewLayout = new HorizontalLayout(); - colorPreviewLayout.setSpacing(true); - colorPreviewLayout.setAlignItems(Alignment.CENTER); - colorPreviewLayout.setWidthFull(); - colorPreviewLayout.getStyle().set("cursor", "pointer"); - - // Farbvorschau-Box - Div colorPreview = new Div(); - colorPreview.getStyle().set("width", "40px").set("height", "30px").set("background-color", currentColor) - .set("border", "1px solid var(--lumo-contrast-30pct)") - .set("border-radius", "var(--lumo-border-radius-m)"); - - Span colorHexLabel = new Span(currentColor); - colorHexLabel.getStyle().set("font-family", "monospace").set("font-size", "var(--lumo-font-size-s)"); - - colorPreviewLayout.add(colorPreview, colorHexLabel); - - // Color Picker Dialog - Dialog colorDialog = DialogStylingHelper - .createStyledDialog(getTranslation("invoicegenerator.color.dialog.title"), "460px"); - colorDialog.setCloseOnOutsideClick(false); - - VerticalLayout dialogLayout = DialogStylingHelper.createContentLayout("320px"); - - // Color Picker im Dialog - Input dialogColorPicker = new Input(); - dialogColorPicker.setType("color"); - dialogColorPicker.setValue(currentColor); - dialogColorPicker.getStyle().set("width", "100%").set("height", "50px").set("padding", "0"); - - // Hex-Eingabe im Dialog - TextField dialogHexField = new TextField(getTranslation("invoicegenerator.color.dialog.hex")); - dialogHexField.setValue(currentColor); - dialogHexField.setWidthFull(); - - // Sync zwischen Color Picker und Hex-Feld - dialogColorPicker.addValueChangeListener(e -> { - dialogHexField.setValue(e.getValue()); - }); - - dialogHexField.addValueChangeListener(e -> { - String newColor = e.getValue(); - if (newColor.matches("^#[0-9A-Fa-f]{6}$")) { - dialogColorPicker.setValue(newColor); - } - }); - - dialogLayout.add(dialogColorPicker, dialogHexField); - colorDialog.add(DialogStylingHelper.wrapContent(dialogLayout)); - - // Dialog Buttons - Button dialogCancelButton = new Button(getTranslation("invoicegenerator.button.cancel"), e -> { - colorDialog.close(); - // Reset to original values - dialogColorPicker.setValue(currentColor); - dialogHexField.setValue(currentColor); - }); - dialogCancelButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); - - Button dialogApplyButton = new Button(getTranslation("invoicegenerator.button.apply"), e -> { - String newColor = dialogColorPicker.getValue(); - // Update preview - colorPreview.getStyle().set("background-color", newColor); - colorHexLabel.setText(newColor); - // Apply to element - getElement().executeJs("if (window.invoiceGenerator) { window.invoiceGenerator.updateElementColor('" - + elementId + "', $0); }", newColor); - colorDialog.close(); - showNotification(getTranslation("invoicegenerator.notification.color.applied")); - }); - dialogApplyButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - - colorDialog.getFooter().add(dialogCancelButton, dialogApplyButton); - - // Öffne Dialog beim Klick auf die Vorschau - Runnable openColorDialog = () -> { - // Aktualisiere Dialog mit aktuellem Wert - String actualColor = colorHexLabel.getText(); - dialogColorPicker.setValue(actualColor); - dialogHexField.setValue(actualColor); - colorDialog.open(); - }; - - colorPreviewLayout.addClickListener(e -> openColorDialog.run()); - colorPreview.addClickListener(e -> openColorDialog.run()); - colorHexLabel.addClickListener(e -> openColorDialog.run()); - - propertiesPanel.add(colorPreviewLayout); - } - - // Löschen Button - Button deleteButton = new Button(getTranslation("invoicegenerator.button.delete"), - new Icon(VaadinIcon.TRASH)); - deleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY); - deleteButton.setWidthFull(); - deleteButton.addClickListener(e -> { - getElement().executeJs( - "if (window.invoiceGenerator) { window.invoiceGenerator.deleteElement('" + elementId + "'); }"); - resetPropertiesPanel(); - }); - propertiesPanel.add(deleteButton); - })); - } - - /** - * Wird von JavaScript aufgerufen wenn die Auswahl aufgehoben wird - */ - @ClientCallable - public void resetPropertiesPanel() { - getUI().ifPresent(ui -> ui.access(() -> { - propertiesPanel.removeAll(); - - Span header = new Span("Eigenschaften"); - header.getStyle().set("font-weight", "bold").set("font-size", "var(--lumo-font-size-l)"); - - selectedElementInfo = new Div(); - selectedElementInfo - .setText("Klicken Sie auf ein Element im Canvas, um dessen Eigenschaften zu bearbeiten."); - selectedElementInfo.getStyle().set("color", "var(--lumo-secondary-text-color)").set("font-size", - "var(--lumo-font-size-s)"); - - propertiesPanel.add(header, selectedElementInfo); - })); - } - @Override public String getPageTitle() { return getTranslation("page.title.invoice.generator"); diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/view/JobManualCompleteView.java b/backend/src/main/java/de/assecutor/votianlt/pages/view/JobManualCompleteView.java index 84efd32..56d5767 100644 --- a/backend/src/main/java/de/assecutor/votianlt/pages/view/JobManualCompleteView.java +++ b/backend/src/main/java/de/assecutor/votianlt/pages/view/JobManualCompleteView.java @@ -31,11 +31,13 @@ import de.assecutor.votianlt.model.JobServiceSelection; import de.assecutor.votianlt.model.JobStatus; import de.assecutor.votianlt.model.Service; import de.assecutor.votianlt.model.User; +import de.assecutor.votianlt.messaging.MessagingPublisher; import de.assecutor.votianlt.pages.base.ui.component.DialogStylingHelper; import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar; import de.assecutor.votianlt.repository.ServiceRepository; import de.assecutor.votianlt.repository.JobRepository; import de.assecutor.votianlt.security.SecurityService; +import de.assecutor.votianlt.service.ClientConnectionService; import de.assecutor.votianlt.service.JobHistoryService; import jakarta.annotation.security.RolesAllowed; import lombok.extern.slf4j.Slf4j; @@ -47,6 +49,7 @@ import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.Map; @Route(value = "job_manual_complete", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class) @RolesAllowed("USER") @@ -67,6 +70,8 @@ public class JobManualCompleteView extends Main implements HasUrlParameter payload = Map.of("type", "job_deleted", "jobId", job.getId().toHexString(), "jobNumber", + job.getJobNumber() != null ? job.getJobNumber() : "", "deletedAt", LocalDateTime.now().toString()); + + log.info("[JOB] Sending job_deleted to {}: {}", appUserId, payload); + messagingPublisher.publishAsJson(appUserId, "job_deleted", payload); + } } diff --git a/backend/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java b/backend/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java index 90dfc11..14c7f4d 100644 --- a/backend/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java +++ b/backend/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java @@ -248,7 +248,7 @@ public class CustomerInvoiceService { * representation of the canvas elements and converts it to PDF. */ public byte[] generatePdfFromCanvasTemplate(String jsonTemplateData) throws Exception { - return generatePdfFromCanvasTemplate(jsonTemplateData, null, null); + return generatePdfFromCanvasTemplate(jsonTemplateData, (de.assecutor.votianlt.model.User) null, null); } public byte[] generatePdfFromCanvasTemplate(String jsonTemplateData, de.assecutor.votianlt.model.User user) @@ -265,27 +265,6 @@ public class CustomerInvoiceService { String invoicePrefix, BigDecimal vatRate) throws Exception { BigDecimal effectiveVatRate = vatRate != null ? vatRate : (user != null && user.getVatRate() != null ? user.getVatRate() : new BigDecimal("0.19")); - // Parse the JSON template data - com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); - com.fasterxml.jackson.databind.JsonNode rootNode = mapper.readTree(jsonTemplateData); - com.fasterxml.jackson.databind.JsonNode elements = rootNode.get("elements"); - - // Build HTML content from canvas elements - StringBuilder htmlBuilder = new StringBuilder(); - htmlBuilder.append(""); - htmlBuilder.append(""); - htmlBuilder.append(""); - htmlBuilder.append(""); - htmlBuilder.append(""); // Prepare variable substitution map java.util.Map variables = new java.util.HashMap<>(); @@ -321,6 +300,39 @@ public class CustomerInvoiceService { variables.put("customer.email", "kunde@beispiel.de"); variables.put("customer.phone", "0987 654321"); + return generatePdfFromCanvasTemplate(jsonTemplateData, variables, effectiveVatRate); + } + + /** + * Render a canvas template PDF using a pre-built variable substitution map. + * Used e.g. for the system invoice template where the issuer data comes from + * SystemInvoiceData instead of a User. + */ + public byte[] generatePdfFromCanvasTemplate(String jsonTemplateData, java.util.Map variables, + BigDecimal vatRate) throws Exception { + BigDecimal effectiveVatRate = vatRate != null ? vatRate : new BigDecimal("0.19"); + // Parse the JSON template data + com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); + com.fasterxml.jackson.databind.JsonNode rootNode = mapper.readTree(jsonTemplateData); + com.fasterxml.jackson.databind.JsonNode elements = rootNode.get("elements"); + + // Build HTML content from canvas elements + StringBuilder htmlBuilder = new StringBuilder(); + htmlBuilder.append(""); + htmlBuilder.append(""); + htmlBuilder.append(""); + htmlBuilder.append(""); + htmlBuilder.append(""); + if (elements != null && elements.isArray()) { for (com.fasterxml.jackson.databind.JsonNode element : elements) { String type = element.has("type") ? element.get("type").asText("text") : "text"; diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index ad53bf9..e3b1f50 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -73,21 +73,11 @@ app.version=@project.version@ app.google.maps.api-key=AIzaSyDnbitL06iLp3elmj-WtPudCykX9xvXcVE # =========================================== -# LLM Configuration (LM Studio) +# LLM Configuration (Anthropic Claude) # =========================================== -app.ai.lmstudio.base-url=${LMSTUDIO_URL} -app.ai.lmstudio.model=${LMSTUDIO_MODEL} -app.ai.lmstudio.htaccess-username=${LMSTUDIO_HTACCESS_USERNAME} -app.ai.lmstudio.htaccess-password=${LMSTUDIO_HTACCESS_PASSWORD} - -# Spring AI OpenAI properties (Pflicht für Auto-Configuration, werden vom LlmRestClient überschrieben) -spring.ai.openai.base-url=${LMSTUDIO_URL} -spring.ai.openai.api-key=not-used -spring.ai.openai.chat.options.model=${LMSTUDIO_MODEL} -spring.ai.openai.chat.options.temperature=0.7 -spring.ai.openai.chat.options.stream=false -spring.ai.openai.connect-timeout=10s -spring.ai.openai.read-timeout=120s +# API-Key kann per Umgebungsvariable ANTHROPIC_API_KEY ueberschrieben werden +app.ai.anthropic.api-key=${ANTHROPIC_API_KEY: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} +app.ai.anthropic.model=${ANTHROPIC_MODEL:claude-opus-4-8} # =========================================== # MCP Server Configuration diff --git a/backend/src/main/resources/messages_de.properties b/backend/src/main/resources/messages_de.properties index aa4e5b2..3003993 100644 --- a/backend/src/main/resources/messages_de.properties +++ b/backend/src/main/resources/messages_de.properties @@ -241,7 +241,7 @@ page.title.dashboard=VotianLT - Dashboard page.title.appuser.create=Neuen App-Nutzer anlegen page.title.messages=Nachrichten page.title.register=Bei VotianLT registrieren -page.title.customers=Kunden +page.title.customers=Adressbuch page.title.customer.edit=Adresse bearbeiten page.title.verwaltung=Verwaltung page.title.company.create=Neue Firma anlegen @@ -249,7 +249,7 @@ page.title.imprint=Impressum page.title.profile.edit=Profil bearbeiten page.title.admin.dashboard=Admin Dashboard page.title.invoice.create=Rechnung erstellen -page.title.customer.create=Neuen Kunden anlegen +page.title.customer.create=Neue Adresse anlegen page.title.login=Bei VotianLT anmelden page.title.jobs=Aufträge page.title.appuser.edit=App-Nutzer bearbeiten @@ -328,9 +328,9 @@ editappuser.dialog.delete.text=Möchten Sie diesen App-Nutzer wirklich löschen? editappuser.dialog.delete.confirm=Löschen # Customers -customers.title=Kunden -customers.button.add=Neuen Kunden hinzufügen -customers.hint.click=Klicken Sie auf einen Kunden, um Details zu sehen +customers.title=Adressbuch +customers.button.add=Neue Adresse anlegen +customers.hint.click=Klicken Sie auf einen Adresseintrag, um Details zu sehen customers.column.company=Firma customers.column.name=Name customers.column.email=E-Mail @@ -349,8 +349,8 @@ editcustomer.dialog.delete.text=Möchten Sie diese Adresse wirklich löschen? editcustomer.dialog.delete.confirm=Löschen # Add Customer -addcustomer.title=Neuen Kunden anlegen -addcustomer.button.submit=Kunden anlegen +addcustomer.title=Neue Adresse anlegen +addcustomer.button.submit=Adresse anlegen addcustomer.notification.validation=Bitte füllen Sie alle Pflichtfelder aus addcustomer.notification.success=Kunde erfolgreich angelegt addcustomer.notification.check=Bitte überprüfen Sie Ihre Eingaben @@ -1056,3 +1056,24 @@ adminpricetable.field.revenue=Umsatzbeteiligung adminpricetable.notification.saved=Preistabelle wurde gespeichert adminpricetable.notification.save.error=Fehler beim Speichern: {0} adminpricetable.notification.load.error=Fehler beim Laden: {0} + +# Rechnungsgenerator - System-Template (Rechnungen an Nutzer) +invoicegenerator.issuer.header=Rechnungssteller +invoicegenerator.issuer.website=Webseite +invoicegenerator.issuer.senderline=Absenderzeile +invoicegenerator.issuer.paymentterms=Zahlungsbedingungen +invoicegenerator.issuer.footer=Fußzeile +invoicegenerator.issuer.invoicedate=Rechnungsdatum +invoicegenerator.recipient.header=Empfänger (Nutzer) +invoicegenerator.recipient.company=Nutzer-Firma +invoicegenerator.recipient.name=Nutzer-Name +invoicegenerator.recipient.street=Nutzer-Straße +invoicegenerator.recipient.city=Nutzer-PLZ/Ort +invoicegenerator.recipient.email=Nutzer-E-Mail +invoicegenerator.template.saved=Template gespeichert +invoicegenerator.template.save.error=Fehler beim Speichern des Templates: {0} +invoicegenerator.button.clear=Leeren +invoicegenerator.notification.cleared=Canvas geleert +invoicegenerator.notification.canvas.error=Canvas-Daten konnten nicht gelesen werden +invoicegenerator.notification.preview.error=Fehler bei der Vorschau: {0} +invoicegenerator.pdf.preview.title=PDF-Vorschau diff --git a/backend/src/main/resources/messages_ee.properties b/backend/src/main/resources/messages_ee.properties index 70c421e..17b87d1 100644 --- a/backend/src/main/resources/messages_ee.properties +++ b/backend/src/main/resources/messages_ee.properties @@ -213,7 +213,7 @@ page.title.dashboard=VotianLT - T\u00f6\u00f6laud page.title.appuser.create=Uue \u00e4pikasutaja loomine page.title.messages=S\u00f5numid page.title.register=VotianLT-sse registreerimine -page.title.customers=Kliendid +page.title.customers=Aadressiraamat page.title.customer.edit=Kliendi muutmine page.title.verwaltung=Haldus page.title.company.create=Uue ettev\u00f5tte loomine @@ -221,7 +221,7 @@ page.title.imprint=Impressum page.title.profile.edit=Profiili muutmine page.title.admin.dashboard=Administraatori t\u00f6\u00f6laud page.title.invoice.create=Arve koostamine -page.title.customer.create=Uue kliendi loomine +page.title.customer.create=Uue aadressi loomine page.title.login=VotianLT-sse sisselogimine page.title.jobs=Tellimused page.title.appuser.edit=\u00c4pikasutaja muutmine @@ -292,9 +292,9 @@ editappuser.notification.password.enter=Palun sisestage uus parool editappuser.notification.deleted=\u00c4pikasutaja edukalt kustutatud editappuser.dialog.delete.text=Kas soovite selle \u00e4pikasutaja t\u00f5esti kustutada? editappuser.dialog.delete.confirm=Kustuta -customers.title=Kliendid -customers.button.add=Lisa uus klient -customers.hint.click=Kl\u00f5psake kliendil, et n\u00e4ha \u00fcksikasju +customers.title=Aadressiraamat +customers.button.add=Lisa uus aadress +customers.hint.click=Kl\u00f5psake aadressikirjel, et n\u00e4ha \u00fcksikasju customers.column.company=Ettev\u00f5te customers.column.name=Nimi customers.column.email=E-post @@ -309,8 +309,8 @@ editcustomer.notification.check=Palun kontrollige oma sisestusi editcustomer.notification.deleted=Klient edukalt kustutatud editcustomer.dialog.delete.text=Kas soovite selle kliendi t\u00f5esti kustutada? editcustomer.dialog.delete.confirm=Kustuta -addcustomer.title=Uue kliendi loomine -addcustomer.button.submit=Loo klient +addcustomer.title=Uue aadressi loomine +addcustomer.button.submit=Loo aadress addcustomer.notification.validation=Palun t\u00e4itke k\u00f5ik kohustuslikud v\u00e4ljad addcustomer.notification.success=Klient edukalt loodud addcustomer.notification.check=Palun kontrollige oma sisestusi @@ -886,3 +886,24 @@ adminpricetable.field.revenue=K\u00e4ibest osalus adminpricetable.notification.saved=Hinnatabel on salvestatud adminpricetable.notification.save.error=Salvestamisel ilmnes viga: {0} adminpricetable.notification.load.error=Laadimisel ilmnes viga: {0} + +# Arvegeneraator - süsteemi mall (arved kasutajatele) +invoicegenerator.issuer.header=Arve väljastaja +invoicegenerator.issuer.website=Veebileht +invoicegenerator.issuer.senderline=Saatja rida +invoicegenerator.issuer.paymentterms=Maksetingimused +invoicegenerator.issuer.footer=Jalus +invoicegenerator.issuer.invoicedate=Arve kuupäev +invoicegenerator.recipient.header=Saaja (kasutaja) +invoicegenerator.recipient.company=Kasutaja firma +invoicegenerator.recipient.name=Kasutaja nimi +invoicegenerator.recipient.street=Kasutaja tänav +invoicegenerator.recipient.city=Kasutaja linn +invoicegenerator.recipient.email=Kasutaja e-post +invoicegenerator.template.saved=Mall salvestatud +invoicegenerator.template.save.error=Viga malli salvestamisel: {0} +invoicegenerator.button.clear=Tühjenda +invoicegenerator.notification.cleared=Lõuend tühjendatud +invoicegenerator.notification.canvas.error=Lõuendi andmeid ei õnnestunud lugeda +invoicegenerator.notification.preview.error=Eelvaate viga: {0} +invoicegenerator.pdf.preview.title=PDF-i eelvaade diff --git a/backend/src/main/resources/messages_en.properties b/backend/src/main/resources/messages_en.properties index 46f56f1..2661fb1 100644 --- a/backend/src/main/resources/messages_en.properties +++ b/backend/src/main/resources/messages_en.properties @@ -241,7 +241,7 @@ page.title.dashboard=VotianLT - Dashboard page.title.appuser.create=Create New App User page.title.messages=Messages page.title.register=Register at VotianLT -page.title.customers=Customers +page.title.customers=Address Book page.title.customer.edit=Edit Customer page.title.verwaltung=Management page.title.company.create=Create New Company @@ -249,7 +249,7 @@ page.title.imprint=Imprint page.title.profile.edit=Edit Profile page.title.admin.dashboard=Admin Dashboard page.title.invoice.create=Create Invoice -page.title.customer.create=Create New Customer +page.title.customer.create=Create New Address page.title.login=Log In to VotianLT page.title.jobs=Jobs page.title.appuser.edit=Edit App User @@ -328,9 +328,9 @@ editappuser.dialog.delete.text=Are you sure you want to delete this app user? editappuser.dialog.delete.confirm=Delete # Customers -customers.title=Customers -customers.button.add=Add New Customer -customers.hint.click=Click on a customer to see details +customers.title=Address Book +customers.button.add=Add New Address +customers.hint.click=Click on an address entry to see details customers.column.company=Company customers.column.name=Name customers.column.email=Email @@ -349,8 +349,8 @@ editcustomer.dialog.delete.text=Are you sure you want to delete this customer? editcustomer.dialog.delete.confirm=Delete # Add Customer -addcustomer.title=Create New Customer -addcustomer.button.submit=Create Customer +addcustomer.title=Create New Address +addcustomer.button.submit=Create Address addcustomer.notification.validation=Please fill in all required fields addcustomer.notification.success=Customer created successfully addcustomer.notification.check=Please check your input @@ -1056,3 +1056,24 @@ adminpricetable.field.revenue=Revenue Share adminpricetable.notification.saved=Price table has been saved adminpricetable.notification.save.error=Error saving: {0} adminpricetable.notification.load.error=Error loading: {0} + +# Invoice generator - system template (invoices to users) +invoicegenerator.issuer.header=Invoice issuer +invoicegenerator.issuer.website=Website +invoicegenerator.issuer.senderline=Sender line +invoicegenerator.issuer.paymentterms=Payment terms +invoicegenerator.issuer.footer=Footer +invoicegenerator.issuer.invoicedate=Invoice date +invoicegenerator.recipient.header=Recipient (user) +invoicegenerator.recipient.company=User company +invoicegenerator.recipient.name=User name +invoicegenerator.recipient.street=User street +invoicegenerator.recipient.city=User city +invoicegenerator.recipient.email=User email +invoicegenerator.template.saved=Template saved +invoicegenerator.template.save.error=Error saving template: {0} +invoicegenerator.button.clear=Clear +invoicegenerator.notification.cleared=Canvas cleared +invoicegenerator.notification.canvas.error=Could not read canvas data +invoicegenerator.notification.preview.error=Preview error: {0} +invoicegenerator.pdf.preview.title=PDF preview diff --git a/backend/src/main/resources/messages_es.properties b/backend/src/main/resources/messages_es.properties index 2bdcb1b..a31512b 100644 --- a/backend/src/main/resources/messages_es.properties +++ b/backend/src/main/resources/messages_es.properties @@ -240,7 +240,7 @@ page.title.dashboard=VotianLT - Panel de control page.title.appuser.create=Crear nuevo usuario de la app page.title.messages=Mensajes page.title.register=Registrarse en VotianLT -page.title.customers=Clientes +page.title.customers=Libreta de direcciones page.title.customer.edit=Editar cliente page.title.verwaltung=Administraci\u00f3n page.title.company.create=Crear nueva empresa @@ -248,7 +248,7 @@ page.title.imprint=Aviso legal page.title.profile.edit=Editar perfil page.title.admin.dashboard=Panel de administraci\u00f3n page.title.invoice.create=Crear factura -page.title.customer.create=Crear nuevo cliente +page.title.customer.create=Crear nueva dirección page.title.login=Iniciar sesi\u00f3n en VotianLT page.title.jobs=Pedidos page.title.appuser.edit=Editar usuario de la app @@ -327,9 +327,9 @@ editappuser.dialog.delete.text=\u00bfDesea realmente eliminar este usuario de la editappuser.dialog.delete.confirm=Eliminar # Customers -customers.title=Clientes -customers.button.add=A\u00f1adir nuevo cliente -customers.hint.click=Haga clic en un cliente para ver los detalles +customers.title=Libreta de direcciones +customers.button.add=A\u00f1adir nueva direcci\u00f3n +customers.hint.click=Haga clic en una entrada de direcci\u00f3n para ver los detalles customers.column.company=Empresa customers.column.name=Nombre customers.column.email=Correo electr\u00f3nico @@ -348,8 +348,8 @@ editcustomer.dialog.delete.text=\u00bfDesea realmente eliminar este cliente? editcustomer.dialog.delete.confirm=Eliminar # Add Customer -addcustomer.title=Crear nuevo cliente -addcustomer.button.submit=Crear cliente +addcustomer.title=Crear nueva dirección +addcustomer.button.submit=Crear dirección addcustomer.notification.validation=Por favor, complete todos los campos obligatorios addcustomer.notification.success=Cliente creado correctamente addcustomer.notification.check=Por favor, revise sus datos @@ -987,3 +987,24 @@ adminpricetable.field.revenue=Participaci\u00f3n en los ingresos adminpricetable.notification.saved=La tabla de precios ha sido guardada adminpricetable.notification.save.error=Error al guardar: {0} adminpricetable.notification.load.error=Error al cargar: {0} + +# Generador de facturas - plantilla del sistema (facturas a usuarios) +invoicegenerator.issuer.header=Emisor de la factura +invoicegenerator.issuer.website=Sitio web +invoicegenerator.issuer.senderline=Línea de remitente +invoicegenerator.issuer.paymentterms=Condiciones de pago +invoicegenerator.issuer.footer=Pie de página +invoicegenerator.issuer.invoicedate=Fecha de la factura +invoicegenerator.recipient.header=Destinatario (usuario) +invoicegenerator.recipient.company=Empresa del usuario +invoicegenerator.recipient.name=Nombre del usuario +invoicegenerator.recipient.street=Calle del usuario +invoicegenerator.recipient.city=Ciudad del usuario +invoicegenerator.recipient.email=Correo del usuario +invoicegenerator.template.saved=Plantilla guardada +invoicegenerator.template.save.error=Error al guardar la plantilla: {0} +invoicegenerator.button.clear=Vaciar +invoicegenerator.notification.cleared=Lienzo vaciado +invoicegenerator.notification.canvas.error=No se pudieron leer los datos del lienzo +invoicegenerator.notification.preview.error=Error de vista previa: {0} +invoicegenerator.pdf.preview.title=Vista previa del PDF diff --git a/backend/src/main/resources/messages_fr.properties b/backend/src/main/resources/messages_fr.properties index 2e159d5..6ae577c 100644 --- a/backend/src/main/resources/messages_fr.properties +++ b/backend/src/main/resources/messages_fr.properties @@ -240,7 +240,7 @@ page.title.dashboard=VotianLT - Tableau de bord page.title.appuser.create=Cr\u00e9er un nouvel utilisateur d'app page.title.messages=Messages page.title.register=S'inscrire sur VotianLT -page.title.customers=Clients +page.title.customers=Carnet d'adresses page.title.customer.edit=Modifier le client page.title.verwaltung=Administration page.title.company.create=Cr\u00e9er une nouvelle entreprise @@ -248,7 +248,7 @@ page.title.imprint=Mentions l\u00e9gales page.title.profile.edit=Modifier le profil page.title.admin.dashboard=Tableau de bord administrateur page.title.invoice.create=Cr\u00e9er une facture -page.title.customer.create=Cr\u00e9er un nouveau client +page.title.customer.create=Cr\u00e9er une nouvelle adresse page.title.login=Se connecter \u00e0 VotianLT page.title.jobs=Missions page.title.appuser.edit=Modifier l'utilisateur d'app @@ -327,9 +327,9 @@ editappuser.dialog.delete.text=Voulez-vous vraiment supprimer cet utilisateur d' editappuser.dialog.delete.confirm=Supprimer # Customers -customers.title=Clients -customers.button.add=Ajouter un nouveau client -customers.hint.click=Cliquez sur un client pour voir les d\u00e9tails +customers.title=Carnet d'adresses +customers.button.add=Ajouter une nouvelle adresse +customers.hint.click=Cliquez sur une entr\u00e9e d'adresse pour voir les d\u00e9tails customers.column.company=Entreprise customers.column.name=Nom customers.column.email=E-mail @@ -348,8 +348,8 @@ editcustomer.dialog.delete.text=Voulez-vous vraiment supprimer ce client ? editcustomer.dialog.delete.confirm=Supprimer # Add Customer -addcustomer.title=Cr\u00e9er un nouveau client -addcustomer.button.submit=Cr\u00e9er le client +addcustomer.title=Cr\u00e9er une nouvelle adresse +addcustomer.button.submit=Cr\u00e9er l'adresse addcustomer.notification.validation=Veuillez remplir tous les champs obligatoires addcustomer.notification.success=Client cr\u00e9\u00e9 avec succ\u00e8s addcustomer.notification.check=Veuillez v\u00e9rifier vos saisies @@ -987,3 +987,24 @@ adminpricetable.field.revenue=Participation au chiffre d'affaires adminpricetable.notification.saved=Le tableau des prix a \u00e9t\u00e9 enregistr\u00e9 adminpricetable.notification.save.error=Erreur lors de l'enregistrement : {0} adminpricetable.notification.load.error=Erreur lors du chargement : {0} + +# Générateur de factures - modèle système (factures aux utilisateurs) +invoicegenerator.issuer.header=Émetteur de la facture +invoicegenerator.issuer.website=Site web +invoicegenerator.issuer.senderline=Ligne d'expéditeur +invoicegenerator.issuer.paymentterms=Conditions de paiement +invoicegenerator.issuer.footer=Pied de page +invoicegenerator.issuer.invoicedate=Date de la facture +invoicegenerator.recipient.header=Destinataire (utilisateur) +invoicegenerator.recipient.company=Société de l'utilisateur +invoicegenerator.recipient.name=Nom de l'utilisateur +invoicegenerator.recipient.street=Rue de l'utilisateur +invoicegenerator.recipient.city=Ville de l'utilisateur +invoicegenerator.recipient.email=E-mail de l'utilisateur +invoicegenerator.template.saved=Modèle enregistré +invoicegenerator.template.save.error=Erreur lors de l''enregistrement du modèle : {0} +invoicegenerator.button.clear=Vider +invoicegenerator.notification.cleared=Canevas vidé +invoicegenerator.notification.canvas.error=Impossible de lire les données du canevas +invoicegenerator.notification.preview.error=Erreur d''aperçu : {0} +invoicegenerator.pdf.preview.title=Aperçu PDF diff --git a/backend/src/main/resources/messages_lt.properties b/backend/src/main/resources/messages_lt.properties index 3259833..3fbeaef 100644 --- a/backend/src/main/resources/messages_lt.properties +++ b/backend/src/main/resources/messages_lt.properties @@ -240,7 +240,7 @@ page.title.dashboard=VotianLT - Valdymo skydas page.title.appuser.create=Sukurti naują programėlės naudotoją page.title.messages=Žinutės page.title.register=Registracija VotianLT -page.title.customers=Klientai +page.title.customers=Adresų knyga page.title.customer.edit=Redaguoti klientą page.title.verwaltung=Administravimas page.title.company.create=Sukurti naują įmonę @@ -248,7 +248,7 @@ page.title.imprint=Informacija page.title.profile.edit=Redaguoti profilį page.title.admin.dashboard=Administratoriaus valdymo skydas page.title.invoice.create=Sukurti sąskaitą -page.title.customer.create=Sukurti naują klientą +page.title.customer.create=Sukurti naują adresą page.title.login=Prisijungti prie VotianLT page.title.jobs=Užsakymai page.title.appuser.edit=Redaguoti programėlės naudotoją @@ -327,9 +327,9 @@ editappuser.dialog.delete.text=Ar tikrai norite ištrinti šį programėlės nau editappuser.dialog.delete.confirm=Ištrinti # Customers -customers.title=Klientai -customers.button.add=Pridėti naują klientą -customers.hint.click=Spustelėkite klientą, kad pamatytumėte informaciją +customers.title=Adresų knyga +customers.button.add=Pridėti naują adresą +customers.hint.click=Spustelėkite adreso įrašą, kad pamatytumėte informaciją customers.column.company=Įmonė customers.column.name=Pavadinimas customers.column.email=El. paštas @@ -348,8 +348,8 @@ editcustomer.dialog.delete.text=Ar tikrai norite ištrinti šį klientą? editcustomer.dialog.delete.confirm=Ištrinti # Add Customer -addcustomer.title=Sukurti naują klientą -addcustomer.button.submit=Sukurti klientą +addcustomer.title=Sukurti naują adresą +addcustomer.button.submit=Sukurti adresą addcustomer.notification.validation=Prašome užpildyti visus privalomus laukus addcustomer.notification.success=Klientas sėkmingai sukurtas addcustomer.notification.check=Prašome patikrinti savo įvestį @@ -989,3 +989,24 @@ adminpricetable.field.revenue=Pajamų dalis adminpricetable.notification.saved=Kainų lentelė išsaugota adminpricetable.notification.save.error=Klaida išsaugant: {0} adminpricetable.notification.load.error=Klaida įkeliant: {0} + +# Sąskaitų generatorius - sistemos šablonas (sąskaitos naudotojams) +invoicegenerator.issuer.header=Sąskaitos išrašytojas +invoicegenerator.issuer.website=Svetainė +invoicegenerator.issuer.senderline=Siuntėjo eilutė +invoicegenerator.issuer.paymentterms=Mokėjimo sąlygos +invoicegenerator.issuer.footer=Poraštė +invoicegenerator.issuer.invoicedate=Sąskaitos data +invoicegenerator.recipient.header=Gavėjas (naudotojas) +invoicegenerator.recipient.company=Naudotojo įmonė +invoicegenerator.recipient.name=Naudotojo vardas +invoicegenerator.recipient.street=Naudotojo gatvė +invoicegenerator.recipient.city=Naudotojo miestas +invoicegenerator.recipient.email=Naudotojo el. paštas +invoicegenerator.template.saved=Šablonas išsaugotas +invoicegenerator.template.save.error=Klaida išsaugant šabloną: {0} +invoicegenerator.button.clear=Išvalyti +invoicegenerator.notification.cleared=Drobė išvalyta +invoicegenerator.notification.canvas.error=Nepavyko nuskaityti drobės duomenų +invoicegenerator.notification.preview.error=Peržiūros klaida: {0} +invoicegenerator.pdf.preview.title=PDF peržiūra diff --git a/backend/src/main/resources/messages_lv.properties b/backend/src/main/resources/messages_lv.properties index 2440fdf..f0e6f2a 100644 --- a/backend/src/main/resources/messages_lv.properties +++ b/backend/src/main/resources/messages_lv.properties @@ -240,7 +240,7 @@ page.title.dashboard=VotianLT - Informācijas panelis page.title.appuser.create=Izveidot jaunu lietotnes lietotāju page.title.messages=Ziņojumi page.title.register=Reģistrēties VotianLT -page.title.customers=Klienti +page.title.customers=Adrešu grāmata page.title.customer.edit=Rediģēt klientu page.title.verwaltung=Pārvaldība page.title.company.create=Izveidot jaunu uzņēmumu @@ -248,7 +248,7 @@ page.title.imprint=Impressum page.title.profile.edit=Rediģēt profilu page.title.admin.dashboard=Administrācijas panelis page.title.invoice.create=Izveidot rēķinu -page.title.customer.create=Izveidot jaunu klientu +page.title.customer.create=Izveidot jaunu adresi page.title.login=Pieteikties VotianLT page.title.jobs=Uzdevumi page.title.appuser.edit=Rediģēt lietotnes lietotāju @@ -327,9 +327,9 @@ editappuser.dialog.delete.text=Vai tiešām vēlaties dzēst šo lietotnes lieto editappuser.dialog.delete.confirm=Dzēst # Customers -customers.title=Klienti -customers.button.add=Pievienot jaunu klientu -customers.hint.click=Noklikšķiniet uz klienta, lai redzētu detaļas +customers.title=Adrešu grāmata +customers.button.add=Pievienot jaunu adresi +customers.hint.click=Noklikšķiniet uz adreses ieraksta, lai redzētu detaļas customers.column.company=Uzņēmums customers.column.name=Nosaukums customers.column.email=E-pasts @@ -348,8 +348,8 @@ editcustomer.dialog.delete.text=Vai tiešām vēlaties dzēst šo klientu? editcustomer.dialog.delete.confirm=Dzēst # Add Customer -addcustomer.title=Izveidot jaunu klientu -addcustomer.button.submit=Izveidot klientu +addcustomer.title=Izveidot jaunu adresi +addcustomer.button.submit=Izveidot adresi addcustomer.notification.validation=Lūdzu, aizpildiet visus obligātos laukus addcustomer.notification.success=Klients veiksmīgi izveidots addcustomer.notification.check=Lūdzu, pārbaudiet savus ievadītos datus @@ -987,3 +987,24 @@ adminpricetable.field.revenue=Ieņēmumu daļa adminpricetable.notification.saved=Cenu tabula saglabāta adminpricetable.notification.save.error=Kļūda saglabājot: {0} adminpricetable.notification.load.error=Kļūda ielādējot: {0} + +# Rēķinu ģenerators - sistēmas veidne (rēķini lietotājiem) +invoicegenerator.issuer.header=Rēķina izrakstītājs +invoicegenerator.issuer.website=Tīmekļa vietne +invoicegenerator.issuer.senderline=Sūtītāja rinda +invoicegenerator.issuer.paymentterms=Apmaksas nosacījumi +invoicegenerator.issuer.footer=Kājene +invoicegenerator.issuer.invoicedate=Rēķina datums +invoicegenerator.recipient.header=Saņēmējs (lietotājs) +invoicegenerator.recipient.company=Lietotāja uzņēmums +invoicegenerator.recipient.name=Lietotāja vārds +invoicegenerator.recipient.street=Lietotāja iela +invoicegenerator.recipient.city=Lietotāja pilsēta +invoicegenerator.recipient.email=Lietotāja e-pasts +invoicegenerator.template.saved=Veidne saglabāta +invoicegenerator.template.save.error=Kļūda saglabājot veidni: {0} +invoicegenerator.button.clear=Notīrīt +invoicegenerator.notification.cleared=Audekls notīrīts +invoicegenerator.notification.canvas.error=Neizdevās nolasīt audekla datus +invoicegenerator.notification.preview.error=Priekšskatījuma kļūda: {0} +invoicegenerator.pdf.preview.title=PDF priekšskatījums diff --git a/backend/src/main/resources/messages_pl.properties b/backend/src/main/resources/messages_pl.properties index c649f09..c296fa6 100644 --- a/backend/src/main/resources/messages_pl.properties +++ b/backend/src/main/resources/messages_pl.properties @@ -240,7 +240,7 @@ page.title.dashboard=VotianLT - Panel g\u0142\u00f3wny page.title.appuser.create=Dodaj nowego u\u017cytkownika aplikacji page.title.messages=Wiadomo\u015bci page.title.register=Rejestracja w VotianLT -page.title.customers=Klienci +page.title.customers=Książka adresowa page.title.customer.edit=Edytuj klienta page.title.verwaltung=Zarz\u0105dzanie page.title.company.create=Dodaj now\u0105 firm\u0119 @@ -248,7 +248,7 @@ page.title.imprint=Impressum page.title.profile.edit=Edytuj profil page.title.admin.dashboard=Panel administracyjny page.title.invoice.create=Utw\u00f3rz faktur\u0119 -page.title.customer.create=Dodaj nowego klienta +page.title.customer.create=Dodaj nowy adres page.title.login=Logowanie do VotianLT page.title.jobs=Zlecenia page.title.appuser.edit=Edytuj u\u017cytkownika aplikacji @@ -327,9 +327,9 @@ editappuser.dialog.delete.text=Czy na pewno chcesz usun\u0105\u0107 tego u\u017c editappuser.dialog.delete.confirm=Usu\u0144 # Customers -customers.title=Klienci -customers.button.add=Dodaj nowego klienta -customers.hint.click=Kliknij klienta, aby zobaczy\u0107 szczeg\u00f3\u0142y +customers.title=Ksi\u0105\u017cka adresowa +customers.button.add=Dodaj nowy adres +customers.hint.click=Kliknij wpis adresowy, aby zobaczy\u0107 szczeg\u00f3\u0142y customers.column.company=Firma customers.column.name=Nazwa customers.column.email=E-mail @@ -348,8 +348,8 @@ editcustomer.dialog.delete.text=Czy na pewno chcesz usun\u0105\u0107 tego klient editcustomer.dialog.delete.confirm=Usu\u0144 # Add Customer -addcustomer.title=Dodaj nowego klienta -addcustomer.button.submit=Dodaj klienta +addcustomer.title=Dodaj nowy adres +addcustomer.button.submit=Dodaj adres addcustomer.notification.validation=Prosz\u0119 wype\u0142ni\u0107 wszystkie wymagane pola addcustomer.notification.success=Klient zosta\u0142 pomy\u015blnie dodany addcustomer.notification.check=Prosz\u0119 sprawdzi\u0107 wprowadzone dane @@ -987,3 +987,24 @@ adminpricetable.field.revenue=Udzia\u0142 w przychodach adminpricetable.notification.saved=Cennik zosta\u0142 zapisany adminpricetable.notification.save.error=B\u0142\u0105d podczas zapisywania: {0} adminpricetable.notification.load.error=B\u0142\u0105d podczas \u0142adowania: {0} + +# Generator faktur - szablon systemowy (faktury dla użytkowników) +invoicegenerator.issuer.header=Wystawca faktury +invoicegenerator.issuer.website=Strona internetowa +invoicegenerator.issuer.senderline=Linia nadawcy +invoicegenerator.issuer.paymentterms=Warunki płatności +invoicegenerator.issuer.footer=Stopka +invoicegenerator.issuer.invoicedate=Data faktury +invoicegenerator.recipient.header=Odbiorca (użytkownik) +invoicegenerator.recipient.company=Firma użytkownika +invoicegenerator.recipient.name=Nazwisko użytkownika +invoicegenerator.recipient.street=Ulica użytkownika +invoicegenerator.recipient.city=Miasto użytkownika +invoicegenerator.recipient.email=E-mail użytkownika +invoicegenerator.template.saved=Szablon zapisany +invoicegenerator.template.save.error=Błąd podczas zapisywania szablonu: {0} +invoicegenerator.button.clear=Wyczyść +invoicegenerator.notification.cleared=Obszar roboczy wyczyszczony +invoicegenerator.notification.canvas.error=Nie można odczytać danych obszaru roboczego +invoicegenerator.notification.preview.error=Błąd podglądu: {0} +invoicegenerator.pdf.preview.title=Podgląd PDF diff --git a/backend/src/main/resources/messages_ru.properties b/backend/src/main/resources/messages_ru.properties index d26b1b6..926a290 100644 --- a/backend/src/main/resources/messages_ru.properties +++ b/backend/src/main/resources/messages_ru.properties @@ -240,7 +240,7 @@ page.title.dashboard=VotianLT - Панель управления page.title.appuser.create=Создать нового пользователя приложения page.title.messages=Сообщения page.title.register=Регистрация в VotianLT -page.title.customers=Клиенты +page.title.customers=Адресная книга page.title.customer.edit=Редактирование клиента page.title.verwaltung=Управление page.title.company.create=Создать новую компанию @@ -248,7 +248,7 @@ page.title.imprint=Выходные данные page.title.profile.edit=Редактирование профиля page.title.admin.dashboard=Панель администратора page.title.invoice.create=Создать счёт -page.title.customer.create=Создать нового клиента +page.title.customer.create=Создать новый адрес page.title.login=Вход в VotianLT page.title.jobs=Заказы page.title.appuser.edit=Редактирование пользователя приложения @@ -327,9 +327,9 @@ editappuser.dialog.delete.text=Вы действительно хотите уд editappuser.dialog.delete.confirm=Удалить # Customers -customers.title=Клиенты -customers.button.add=Добавить нового клиента -customers.hint.click=Нажмите на клиента, чтобы увидеть подробности +customers.title=Адресная книга +customers.button.add=Добавить новый адрес +customers.hint.click=Нажмите на запись адреса, чтобы увидеть подробности customers.column.company=Компания customers.column.name=Имя customers.column.email=Эл. почта @@ -348,8 +348,8 @@ editcustomer.dialog.delete.text=Вы действительно хотите у editcustomer.dialog.delete.confirm=Удалить # Add Customer -addcustomer.title=Создать нового клиента -addcustomer.button.submit=Создать клиента +addcustomer.title=Создать новый адрес +addcustomer.button.submit=Создать адрес addcustomer.notification.validation=Пожалуйста, заполните все обязательные поля addcustomer.notification.success=Клиент успешно создан addcustomer.notification.check=Пожалуйста, проверьте ваши данные @@ -987,3 +987,24 @@ adminpricetable.field.revenue=Участие в выручке adminpricetable.notification.saved=Таблица цен сохранена adminpricetable.notification.save.error=Ошибка при сохранении: {0} adminpricetable.notification.load.error=Ошибка при загрузке: {0} + +# Генератор счетов - системный шаблон (счета пользователям) +invoicegenerator.issuer.header=Выставитель счёта +invoicegenerator.issuer.website=Веб-сайт +invoicegenerator.issuer.senderline=Строка отправителя +invoicegenerator.issuer.paymentterms=Условия оплаты +invoicegenerator.issuer.footer=Нижний колонтитул +invoicegenerator.issuer.invoicedate=Дата счёта +invoicegenerator.recipient.header=Получатель (пользователь) +invoicegenerator.recipient.company=Компания пользователя +invoicegenerator.recipient.name=Имя пользователя +invoicegenerator.recipient.street=Улица пользователя +invoicegenerator.recipient.city=Город пользователя +invoicegenerator.recipient.email=E-mail пользователя +invoicegenerator.template.saved=Шаблон сохранён +invoicegenerator.template.save.error=Ошибка при сохранении шаблона: {0} +invoicegenerator.button.clear=Очистить +invoicegenerator.notification.cleared=Холст очищен +invoicegenerator.notification.canvas.error=Не удалось прочитать данные холста +invoicegenerator.notification.preview.error=Ошибка предпросмотра: {0} +invoicegenerator.pdf.preview.title=Предпросмотр PDF diff --git a/backend/src/main/resources/messages_tr.properties b/backend/src/main/resources/messages_tr.properties index bae9798..7db2e13 100644 --- a/backend/src/main/resources/messages_tr.properties +++ b/backend/src/main/resources/messages_tr.properties @@ -240,7 +240,7 @@ page.title.dashboard=VotianLT - Kontrol Paneli page.title.appuser.create=Yeni Uygulama Kullan\u0131c\u0131s\u0131 Olu\u015ftur page.title.messages=Mesajlar page.title.register=VotianLT'ye Kay\u0131t Ol -page.title.customers=M\u00fc\u015fteriler +page.title.customers=Adres defteri page.title.customer.edit=M\u00fc\u015fteriyi D\u00fczenle page.title.verwaltung=Y\u00f6netim page.title.company.create=Yeni \u015eirket Olu\u015ftur @@ -248,7 +248,7 @@ page.title.imprint=K\u00fcnye page.title.profile.edit=Profili D\u00fczenle page.title.admin.dashboard=Y\u00f6netici Kontrol Paneli page.title.invoice.create=Fatura Olu\u015ftur -page.title.customer.create=Yeni M\u00fc\u015fteri Olu\u015ftur +page.title.customer.create=Yeni Adres Olu\u015ftur page.title.login=VotianLT'ye Giri\u015f Yap page.title.jobs=\u0130\u015fler page.title.appuser.edit=Uygulama Kullan\u0131c\u0131s\u0131n\u0131 D\u00fczenle @@ -327,9 +327,9 @@ editappuser.dialog.delete.text=Bu uygulama kullan\u0131c\u0131s\u0131n\u0131 sil editappuser.dialog.delete.confirm=Sil # Customers -customers.title=M\u00fc\u015fteriler -customers.button.add=Yeni M\u00fc\u015fteri Ekle -customers.hint.click=Ayr\u0131nt\u0131lar\u0131 g\u00f6rmek i\u00e7in bir m\u00fc\u015fteriye t\u0131klay\u0131n +customers.title=Adres defteri +customers.button.add=Yeni Adres Ekle +customers.hint.click=Ayr\u0131nt\u0131lar\u0131 g\u00f6rmek i\u00e7in bir adres giri\u015fine t\u0131klay\u0131n customers.column.company=\u015eirket customers.column.name=Ad customers.column.email=E-Posta @@ -348,8 +348,8 @@ editcustomer.dialog.delete.text=Bu m\u00fc\u015fteriyi silmek istedi\u011finizde editcustomer.dialog.delete.confirm=Sil # Add Customer -addcustomer.title=Yeni M\u00fc\u015fteri Olu\u015ftur -addcustomer.button.submit=M\u00fc\u015fteri Olu\u015ftur +addcustomer.title=Yeni Adres Olu\u015ftur +addcustomer.button.submit=Adres Olu\u015ftur addcustomer.notification.validation=L\u00fctfen t\u00fcm zorunlu alanlar\u0131 doldurun addcustomer.notification.success=M\u00fc\u015fteri ba\u015far\u0131yla olu\u015fturuldu addcustomer.notification.check=L\u00fctfen giri\u015flerinizi kontrol edin @@ -987,3 +987,24 @@ adminpricetable.field.revenue=Gelir Pay\u0131 adminpricetable.notification.saved=Fiyat tablosu kaydedildi adminpricetable.notification.save.error=Kaydetme hatas\u0131: {0} adminpricetable.notification.load.error=Y\u00fckleme hatas\u0131: {0} + +# Fatura oluşturucu - sistem şablonu (kullanıcılara faturalar) +invoicegenerator.issuer.header=Fatura düzenleyen +invoicegenerator.issuer.website=Web sitesi +invoicegenerator.issuer.senderline=Gönderen satırı +invoicegenerator.issuer.paymentterms=Ödeme koşulları +invoicegenerator.issuer.footer=Alt bilgi +invoicegenerator.issuer.invoicedate=Fatura tarihi +invoicegenerator.recipient.header=Alıcı (kullanıcı) +invoicegenerator.recipient.company=Kullanıcı firması +invoicegenerator.recipient.name=Kullanıcı adı +invoicegenerator.recipient.street=Kullanıcı sokağı +invoicegenerator.recipient.city=Kullanıcı şehri +invoicegenerator.recipient.email=Kullanıcı e-postası +invoicegenerator.template.saved=Şablon kaydedildi +invoicegenerator.template.save.error=Şablon kaydedilirken hata: {0} +invoicegenerator.button.clear=Temizle +invoicegenerator.notification.cleared=Tuval temizlendi +invoicegenerator.notification.canvas.error=Tuval verileri okunamadı +invoicegenerator.notification.preview.error=Önizleme hatası: {0} +invoicegenerator.pdf.preview.title=PDF önizleme