diff --git a/src/main/bundles/dev.bundle b/src/main/bundles/dev.bundle index 14e5388..ccaa944 100644 Binary files a/src/main/bundles/dev.bundle and b/src/main/bundles/dev.bundle differ diff --git a/src/main/frontend/invoice-generator/profile-invoice-generator.js b/src/main/frontend/invoice-generator/profile-invoice-generator.js index 4214a96..f9a353b 100644 --- a/src/main/frontend/invoice-generator/profile-invoice-generator.js +++ b/src/main/frontend/invoice-generator/profile-invoice-generator.js @@ -1,4 +1,12 @@ // Profile Invoice Generator - Initializes canvas on the edit-profile page +// Global state to persist between tab switches +window.profileInvoiceState = window.profileInvoiceState || { + elements: [], + selectedElement: null, + elementCounter: 0, + initialized: false +}; + window.initProfileInvoiceGenerator = function() { var containerId = 'invoice-canvas-container-profile'; var container = document.getElementById(containerId); @@ -8,12 +16,9 @@ window.initProfileInvoiceGenerator = function() { return; } - // Check if canvas already exists - var existingCanvas = container.querySelector('canvas'); - if (existingCanvas) { - console.log('Canvas already exists'); - return; - } + // Always clear and recreate canvas to ensure clean state + container.innerHTML = ''; + window.profileInvoiceState.initialized = false; // Get container dimensions var rect = container.getBoundingClientRect(); @@ -31,13 +36,13 @@ window.initProfileInvoiceGenerator = function() { var ctx = canvas.getContext('2d'); - // State - var elements = []; - var selectedElement = null; + // Restore state from global or initialize + var elements = window.profileInvoiceState.elements; + var selectedElement = window.profileInvoiceState.selectedElement; + var elementCounter = window.profileInvoiceState.elementCounter; var isDragging = false; var dragStart = { x: 0, y: 0 }; var elementStart = { x: 0, y: 0 }; - var elementCounter = 0; var gridSize = 5; // Page dimensions @@ -474,23 +479,34 @@ window.initProfileInvoiceGenerator = function() { } }; + // Save state to global before functions are defined + function saveState() { + window.profileInvoiceState.elements = elements; + window.profileInvoiceState.selectedElement = selectedElement; + window.profileInvoiceState.elementCounter = elementCounter; + } + // Clear canvas function window.clearProfileCanvas = function() { elements = []; selectedElement = null; + window.profileInvoiceState.elements = []; + window.profileInvoiceState.selectedElement = null; notifyElementDeselected(); draw(); }; // Get canvas data window.getProfileCanvasData = function() { + saveState(); return { elements: elements }; }; draw(); - console.log('Profile canvas initialized'); + window.profileInvoiceState.initialized = true; + console.log('Profile canvas initialized with ' + elements.length + ' elements'); // Handle window resize window.addEventListener('resize', function() { @@ -531,4 +547,45 @@ window.initProfileInvoiceGenerator = function() { } }); } + + // Load template function + window.loadProfileTemplate = function(templateData) { + try { + var data = JSON.parse(templateData); + if (data.elements && Array.isArray(data.elements)) { + // Clear existing elements + elements = []; + selectedElement = null; + + // Load new elements + data.elements.forEach(function(el) { + // Ensure element has an ID + if (!el.id) { + elementCounter++; + el.id = 'element-' + elementCounter; + } + elements.push(el); + }); + + // Update counter to be higher than any loaded element + elements.forEach(function(el) { + if (el.id && el.id.startsWith('element-')) { + var num = parseInt(el.id.replace('element-', '')); + if (!isNaN(num) && num > elementCounter) { + elementCounter = num; + } + } + }); + + // Save to global state + saveState(); + + draw(); + notifyElementDeselected(); + console.log('Template loaded with ' + elements.length + ' elements'); + } + } catch (e) { + console.error('Error loading template:', e); + } + }; }; diff --git a/src/main/java/de/assecutor/votianlt/model/InvoiceTemplate.java b/src/main/java/de/assecutor/votianlt/model/InvoiceTemplate.java new file mode 100644 index 0000000..dc4de58 --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/model/InvoiceTemplate.java @@ -0,0 +1,66 @@ +package de.assecutor.votianlt.model; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.time.LocalDateTime; + +/** + * Stores invoice template data for a user. + * Contains the JSON representation of the canvas elements. + */ +@Document(collection = "invoice_templates") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class InvoiceTemplate { + + @Id + private String id; + + /** + * The user ID this template belongs to + */ + private String userId; + + /** + * Template name (optional, for future use if multiple templates are supported) + */ + private String name; + + /** + * JSON string containing the template data (canvas elements) + */ + private String templateData; + + /** + * When the template was created + */ + private LocalDateTime createdAt; + + /** + * When the template was last updated + */ + private LocalDateTime updatedAt; + + /** + * Version for optimistic locking + */ + private Long version; + + public InvoiceTemplate(String userId, String name, String templateData) { + this.userId = userId; + this.name = name; + this.templateData = templateData; + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } + + public void updateTemplate(String templateData) { + this.templateData = templateData; + this.updatedAt = LocalDateTime.now(); + } +} diff --git a/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java b/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java index 2aa85f8..c689439 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java @@ -37,6 +37,7 @@ import de.assecutor.votianlt.pages.service.UserService; import de.assecutor.votianlt.pages.service.UserInvoiceDataService; import de.assecutor.votianlt.security.SecurityService; import de.assecutor.votianlt.service.CustomerInvoiceService; +import de.assecutor.votianlt.service.InvoiceTemplateService; import com.vaadin.flow.component.dependency.JsModule; import com.vaadin.flow.component.ClientCallable; import jakarta.annotation.security.RolesAllowed; @@ -60,14 +61,17 @@ public class EditProfileView extends HorizontalLayout { private final User currentUser; private final UserInvoiceDataService userInvoiceDataService; private final CustomerInvoiceService customerInvoiceService; + private final InvoiceTemplateService invoiceTemplateService; private UserInvoiceData currentInvoiceData; private Checkbox billingEnabled; private VerticalLayout propertiesPanelProfile; public EditProfileView(UserService userService, UserInvoiceDataService userInvoiceDataService, - CustomerInvoiceService customerInvoiceService, SecurityService securityService) { + CustomerInvoiceService customerInvoiceService, InvoiceTemplateService invoiceTemplateService, + SecurityService securityService) { this.userInvoiceDataService = userInvoiceDataService; this.customerInvoiceService = customerInvoiceService; + this.invoiceTemplateService = invoiceTemplateService; this.currentUser = securityService.getCurrentDatabaseUser(); setSizeFull(); setPadding(true); @@ -378,16 +382,20 @@ public class EditProfileView extends HorizontalLayout { saveTemplateButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); saveTemplateButton.addClickListener(e -> { getElement().executeJs( - "if (window.getProfileCanvasData) {" + - " var data = JSON.stringify(window.getProfileCanvasData(), null, 2);" + - " var blob = new Blob([data], { type: 'application/json' });" + - " var url = URL.createObjectURL(blob);" + - " var a = document.createElement('a');" + - " a.href = url;" + - " a.download = 'template.json';" + - " a.click();" + - " URL.revokeObjectURL(url);" + - "}"); + "if (window.getProfileCanvasData) { return JSON.stringify(window.getProfileCanvasData()); } else { return null; }") + .then(result -> { + if (result == null) { + Notification.show("Fehler: Canvas-Daten konnten nicht gelesen werden", 3000, Notification.Position.BOTTOM_CENTER); + return; + } + try { + String templateData = result.toString(); + invoiceTemplateService.saveTemplate(currentUser.getId().toString(), templateData); + Notification.show("Template erfolgreich gespeichert", 3000, Notification.Position.BOTTOM_CENTER); + } catch (Exception ex) { + Notification.show("Fehler beim Speichern: " + ex.getMessage(), 5000, Notification.Position.BOTTOM_CENTER); + } + }); }); actionLayout.add(clearButton, previewPdfButton, saveTemplateButton); @@ -407,6 +415,8 @@ public class EditProfileView extends HorizontalLayout { "setTimeout(function() { " + " if (window.initProfileInvoiceGenerator) { " + " window.initProfileInvoiceGenerator(); " + + " console.log('Canvas initialized, now loading template...'); " + + " $0.$server.onCanvasReady(); " + " } else { " + " console.error('initProfileInvoiceGenerator not found'); " + " } " + @@ -1084,4 +1094,48 @@ public class EditProfileView extends HorizontalLayout { })); } + /** + * Called by JavaScript when the canvas is ready + */ + @ClientCallable + public void onCanvasReady() { + System.out.println("Canvas ready, loading template..."); + loadInvoiceTemplate(); + } + + /** + * Load the saved invoice template from database and display it on the canvas + */ + private void loadInvoiceTemplate() { + try { + java.util.Optional optionalTemplate = + invoiceTemplateService.getTemplateByUserId(currentUser.getId().toString()); + if (optionalTemplate.isPresent()) { + String templateData = optionalTemplate.get().getTemplateData(); + if (templateData != null && !templateData.isEmpty()) { + // Escape single quotes and newlines for JavaScript + String escapedData = templateData + .replace("\\", "\\\\") + .replace("'", "\\'") + .replace("\n", "\\n") + .replace("\r", ""); + getElement().executeJs( + "setTimeout(function() { " + + " if (window.loadProfileTemplate && document.getElementById('invoice-canvas-container-profile')) { " + + " console.log('Loading template into canvas...'); " + + " window.loadProfileTemplate('" + escapedData + "'); " + + " } else { " + + " console.error('loadProfileTemplate or canvas not available'); " + + " } " + + "}, 100);"); + } + } else { + System.out.println("No template found for user: " + currentUser.getId()); + } + } catch (Exception ex) { + System.err.println("Fehler beim Laden des Templates: " + ex.getMessage()); + ex.printStackTrace(); + } + } + } diff --git a/src/main/java/de/assecutor/votianlt/repository/InvoiceTemplateRepository.java b/src/main/java/de/assecutor/votianlt/repository/InvoiceTemplateRepository.java new file mode 100644 index 0000000..ee425b5 --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/repository/InvoiceTemplateRepository.java @@ -0,0 +1,26 @@ +package de.assecutor.votianlt.repository; + +import de.assecutor.votianlt.model.InvoiceTemplate; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface InvoiceTemplateRepository extends MongoRepository { + + /** + * Find the invoice template for a specific user + */ + Optional findByUserId(String userId); + + /** + * Check if a template exists for a user + */ + boolean existsByUserId(String userId); + + /** + * Delete the template for a user + */ + void deleteByUserId(String userId); +} diff --git a/src/main/java/de/assecutor/votianlt/service/InvoiceTemplateService.java b/src/main/java/de/assecutor/votianlt/service/InvoiceTemplateService.java new file mode 100644 index 0000000..4f9a6d7 --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/service/InvoiceTemplateService.java @@ -0,0 +1,52 @@ +package de.assecutor.votianlt.service; + +import de.assecutor.votianlt.model.InvoiceTemplate; +import de.assecutor.votianlt.repository.InvoiceTemplateRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class InvoiceTemplateService { + + private final InvoiceTemplateRepository invoiceTemplateRepository; + + /** + * Save or update the invoice template for a user + */ + public InvoiceTemplate saveTemplate(String userId, String templateData) { + Optional existing = invoiceTemplateRepository.findByUserId(userId); + + if (existing.isPresent()) { + InvoiceTemplate template = existing.get(); + template.updateTemplate(templateData); + return invoiceTemplateRepository.save(template); + } else { + InvoiceTemplate newTemplate = new InvoiceTemplate(userId, "Standard", templateData); + return invoiceTemplateRepository.save(newTemplate); + } + } + + /** + * Get the invoice template for a user + */ + public Optional getTemplateByUserId(String userId) { + return invoiceTemplateRepository.findByUserId(userId); + } + + /** + * Check if a template exists for a user + */ + public boolean hasTemplate(String userId) { + return invoiceTemplateRepository.existsByUserId(userId); + } + + /** + * Delete the template for a user + */ + public void deleteTemplate(String userId) { + invoiceTemplateRepository.deleteByUserId(userId); + } +}