From 435522cf7e22842cfcc6d5901b922619965358a0 Mon Sep 17 00:00:00 2001 From: Sven Carstensen Date: Tue, 23 Sep 2025 14:04:09 +0200 Subject: [PATCH] Erweiterungen --- .../votianlt/model/TaskTemplate.java | 37 ++ .../pages/service/TaskTemplateService.java | 56 +++ .../votianlt/pages/view/AddJobView.java | 405 +++++++++++++++++- .../repository/TaskTemplateRepository.java | 19 + 4 files changed, 512 insertions(+), 5 deletions(-) create mode 100644 src/main/java/de/assecutor/votianlt/model/TaskTemplate.java create mode 100644 src/main/java/de/assecutor/votianlt/pages/service/TaskTemplateService.java create mode 100644 src/main/java/de/assecutor/votianlt/repository/TaskTemplateRepository.java diff --git a/src/main/java/de/assecutor/votianlt/model/TaskTemplate.java b/src/main/java/de/assecutor/votianlt/model/TaskTemplate.java new file mode 100644 index 0000000..ab0d405 --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/model/TaskTemplate.java @@ -0,0 +1,37 @@ +package de.assecutor.votianlt.model; + +import de.assecutor.votianlt.model.task.BaseTask; +import lombok.Data; +import org.bson.types.ObjectId; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.index.Indexed; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +@Document(collection = "task_templates") +public class TaskTemplate { + + @Id + private ObjectId id; + + @Indexed + private ObjectId userId; + + private String templateName; + private List tasks; + + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public TaskTemplate() { + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } + + public void updateTimestamp() { + this.updatedAt = LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/src/main/java/de/assecutor/votianlt/pages/service/TaskTemplateService.java b/src/main/java/de/assecutor/votianlt/pages/service/TaskTemplateService.java new file mode 100644 index 0000000..dda298a --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/pages/service/TaskTemplateService.java @@ -0,0 +1,56 @@ +package de.assecutor.votianlt.pages.service; + +import de.assecutor.votianlt.model.TaskTemplate; +import de.assecutor.votianlt.model.task.BaseTask; +import de.assecutor.votianlt.repository.TaskTemplateRepository; +import org.bson.types.ObjectId; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +public class TaskTemplateService { + + private final TaskTemplateRepository taskTemplateRepository; + + public TaskTemplateService(TaskTemplateRepository taskTemplateRepository) { + this.taskTemplateRepository = taskTemplateRepository; + } + + public List findByUserId(ObjectId userId) { + return taskTemplateRepository.findByUserIdOrderByTemplateNameAsc(userId); + } + + public Optional findByUserIdAndTemplateName(ObjectId userId, String templateName) { + return taskTemplateRepository.findByUserIdAndTemplateName(userId, templateName); + } + + public TaskTemplate save(TaskTemplate taskTemplate) { + taskTemplate.updateTimestamp(); + return taskTemplateRepository.save(taskTemplate); + } + + public TaskTemplate createTemplate(ObjectId userId, String templateName, List tasks) { + // Check if template with same name already exists for this user + Optional existing = findByUserIdAndTemplateName(userId, templateName); + if (existing.isPresent()) { + throw new RuntimeException("Template mit dem Namen '" + templateName + "' existiert bereits"); + } + + TaskTemplate template = new TaskTemplate(); + template.setUserId(userId); + template.setTemplateName(templateName); + template.setTasks(tasks); + + return save(template); + } + + public void deleteByUserIdAndTemplateName(ObjectId userId, String templateName) { + taskTemplateRepository.deleteByUserIdAndTemplateName(userId, templateName); + } + + public boolean templateExists(ObjectId userId, String templateName) { + return findByUserIdAndTemplateName(userId, templateName).isPresent(); + } +} \ No newline at end of file diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java b/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java index b74653c..03d5d3c 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java @@ -4,6 +4,8 @@ import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.checkbox.Checkbox; import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.confirmdialog.ConfirmDialog; +import com.vaadin.flow.component.dialog.Dialog; import com.vaadin.flow.component.datepicker.DatePicker; import com.vaadin.flow.component.html.H2; @@ -44,6 +46,9 @@ import de.assecutor.votianlt.pages.service.AddCustomerService; import de.assecutor.votianlt.model.Customer; import de.assecutor.votianlt.pages.service.AppUserService; import de.assecutor.votianlt.model.AppUser; +import de.assecutor.votianlt.pages.service.TaskTemplateService; +import de.assecutor.votianlt.model.TaskTemplate; +import de.assecutor.votianlt.security.SecurityService; import jakarta.annotation.security.RolesAllowed; import lombok.extern.slf4j.Slf4j; @@ -63,6 +68,8 @@ public class AddJobView extends Main { private final CustomerService customerService; private final AddCustomerService addCustomerService; private final AppUserService appUserService; + private final TaskTemplateService taskTemplateService; + private final SecurityService securityService; // Customer selection private ComboBox customerSelection; @@ -138,11 +145,14 @@ public class AddJobView extends Main { private List availableAppUsers; public AddJobView(AddJobService addJobService, AddCustomerService addCustomerService, - CustomerService customerService, AppUserService appUserService) { + CustomerService customerService, AppUserService appUserService, + TaskTemplateService taskTemplateService, SecurityService securityService) { this.addJobService = addJobService; this.addCustomerService = addCustomerService; this.customerService = customerService; this.appUserService = appUserService; + this.taskTemplateService = taskTemplateService; + this.securityService = securityService; initializeComponents(); setupLayout(); setupValidation(); @@ -1295,10 +1305,37 @@ public class AddJobView extends Main { content.setSpacing(true); content.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH); - // Aufgabentitel + // Aufgabentitel mit Template-Auswahl H3 tasksTitle = new H3("Zu quittierende Aufgaben"); tasksTitle.getStyle().set("margin", "0"); - content.add(tasksTitle); + + ComboBox templateComboBox = new ComboBox<>(); + templateComboBox.setPlaceholder("Template auswählen..."); + templateComboBox.setItemLabelGenerator(TaskTemplate::getTemplateName); + templateComboBox.setClearButtonVisible(true); + // Breite auf verbleibenden Platz einstellen + templateComboBox.setWidthFull(); + + // Load templates for current user + loadTemplatesIntoComboBox(templateComboBox); + + // Handle template selection + templateComboBox.addValueChangeListener(e -> { + if (e.getValue() != null) { + loadTasksFromTemplate(e.getValue(), templateComboBox); + } + }); + + // Icon-Button zum Speichern als Template + Button saveAsTemplateBtn = new Button(new Icon(VaadinIcon.BOOKMARK)); + saveAsTemplateBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + saveAsTemplateBtn.setTooltipText("Aufgaben als Template speichern"); + saveAsTemplateBtn.addClickListener(e -> saveTasksAsTemplate()); + + HorizontalLayout titleWithTemplate = new HorizontalLayout(tasksTitle, templateComboBox, saveAsTemplateBtn); + titleWithTemplate.setAlignItems(FlexComponent.Alignment.CENTER); + titleWithTemplate.setSpacing(true); + content.add(titleWithTemplate); // Dynamische Aufgabenliste tasksList = new VerticalLayout(); @@ -1664,8 +1701,42 @@ public class AddJobView extends Main { addTodoBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY); addTodoBtn.addClickListener(e -> addTodoItem.accept(null)); - // Add initial todo item - addTodoItem.accept(null); + // Load existing todo items from the task, or add one empty item if none exist + if (task instanceof TodoListTask) { + TodoListTask todoTask = (TodoListTask) task; + if (todoTask.getTodoItems() != null && !todoTask.getTodoItems().isEmpty()) { + // Create UI rows for existing todo items + for (String todoText : todoTask.getTodoItems()) { + HorizontalLayout todoRow = new HorizontalLayout(); + todoRow.setWidthFull(); + todoRow.setAlignItems(FlexComponent.Alignment.END); + + TextField todoField = new TextField(); + todoField.setPlaceholder("To-Do Punkt"); + todoField.setWidth("100%"); + todoField.setValue(todoText != null ? todoText : ""); // Set the saved text + + Button removeTodo = new Button(new Icon(VaadinIcon.CLOSE_SMALL)); + removeTodo.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY); + removeTodo.addClickListener(e -> { + todoList.remove(todoRow); + updateTodoItems(todoList, task); + }); + + todoRow.add(todoField, removeTodo); + todoRow.setFlexGrow(1, todoField); + todoList.add(todoRow); + + todoField.addValueChangeListener(ev -> updateTodoItems(todoList, task)); + } + } else { + // Add initial empty todo item if no existing items + addTodoItem.accept(null); + } + } else { + // Add initial empty todo item for new tasks + addTodoItem.accept(null); + } todoContainer.add(todoList, addTodoBtn); configContainer.add(todoContainer); @@ -1746,4 +1817,328 @@ public class AddJobView extends Main { } } + /** + * Speichert die aktuell konfigurierten Aufgaben als Template + */ + private void saveTasksAsTemplate() { + try { + // Check if there are any tasks to save + if (tasksState.isEmpty()) { + Notification.show("Keine Aufgaben zum Speichern vorhanden", 3000, Notification.Position.BOTTOM_END); + return; + } + + // Create dialog for template name input + Dialog dialog = new Dialog(); + dialog.setHeaderTitle("Template speichern"); + dialog.setWidth("400px"); + + VerticalLayout dialogLayout = new VerticalLayout(); + dialogLayout.setPadding(false); + dialogLayout.setSpacing(true); + + TextField templateNameField = new TextField("Template-Name"); + templateNameField.setPlaceholder("Geben Sie einen Namen für das Template ein"); + templateNameField.setWidthFull(); + templateNameField.setRequiredIndicatorVisible(true); + + Button saveButton = new Button("Speichern"); + saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + saveButton.addClickListener(e -> { + String templateName = templateNameField.getValue(); + if (templateName == null || templateName.trim().isEmpty()) { + Notification.show("Bitte geben Sie einen Template-Namen ein", 3000, Notification.Position.BOTTOM_END); + return; + } + + try { + // Create deep copies of current tasks + List tasksCopy = new ArrayList<>(); + for (BaseTask task : tasksState) { + // Create a copy of each task to avoid reference issues + BaseTask taskCopy = createTaskCopy(task); + tasksCopy.add(taskCopy); + } + + // Save template with task type information and specific data + taskTemplateService.createTemplate( + securityService.getCurrentDatabaseUser().getId(), + templateName.trim(), + tasksCopy + ); + + dialog.close(); + Notification.show("Template '" + templateName + "' erfolgreich gespeichert", 3000, Notification.Position.BOTTOM_END); + + } catch (RuntimeException ex) { + Notification.show(ex.getMessage(), 4000, Notification.Position.MIDDLE); + } catch (Exception ex) { + log.error("Error saving task template", ex); + Notification.show("Fehler beim Speichern des Templates: " + ex.getMessage(), 4000, Notification.Position.MIDDLE); + } + }); + + Button cancelButton = new Button("Abbrechen"); + cancelButton.addClickListener(e -> dialog.close()); + + HorizontalLayout buttonLayout = new HorizontalLayout(cancelButton, saveButton); + buttonLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.END); + + dialogLayout.add(templateNameField, buttonLayout); + dialog.add(dialogLayout); + dialog.open(); + + } catch (Exception e) { + log.error("Error opening save template dialog", e); + Notification.show("Fehler beim Öffnen des Dialogs: " + e.getMessage(), 4000, Notification.Position.MIDDLE); + } + } + + /** + * Creates a deep copy of a task to avoid reference issues in templates + * Saves all task-specific data including type and specific properties + */ + private BaseTask createTaskCopy(BaseTask original) { + BaseTask copy = null; + + if (original instanceof ConfirmationTask) { + ConfirmationTask origTask = (ConfirmationTask) original; + copy = new ConfirmationTask(); + // Copy all specific data for ConfirmationTask + if (origTask.getButtonText() != null) { + ((ConfirmationTask) copy).setButtonText(origTask.getButtonText()); + } + } else if (original instanceof SignatureTask) { + // SignatureTask has no specific data beyond the base task + copy = new SignatureTask(); + } else if (original instanceof TodoListTask) { + TodoListTask origTask = (TodoListTask) original; + copy = new TodoListTask(); + // Copy all todo items + if (origTask.getTodoItems() != null) { + ((TodoListTask) copy).setTodoItems(new ArrayList<>(origTask.getTodoItems())); + } + } else if (original instanceof PhotoTask) { + PhotoTask origTask = (PhotoTask) original; + // Copy with all photo-specific parameters + copy = new PhotoTask( + origTask.getMinPhotoCount() != null ? origTask.getMinPhotoCount() : 1, + origTask.getMaxPhotoCount() != null ? origTask.getMaxPhotoCount() : 10 + ); + } else if (original instanceof BarcodeTask) { + BarcodeTask origTask = (BarcodeTask) original; + // Copy with all barcode-specific parameters + copy = new BarcodeTask( + origTask.getMinBarcodeCount() != null ? origTask.getMinBarcodeCount() : 1, + origTask.getMaxBarcodeCount() != null ? origTask.getMaxBarcodeCount() : 10 + ); + } else if (original instanceof CommentTask) { + CommentTask origTask = (CommentTask) original; + // Copy with all comment-specific parameters + copy = new CommentTask( + origTask.getCommentText() != null ? origTask.getCommentText() : "", + origTask.isRequired() + ); + } + + if (copy != null) { + // Copy all base task properties + copy.setTaskOrder(original.getTaskOrder() != null ? original.getTaskOrder() : 0); + copy.setCompleted(original.isCompleted()); + copy.setCompletedAt(original.getCompletedAt()); + copy.setCompletedBy(original.getCompletedBy()); + } + + return copy; + } + + /** + * Loads available templates into the ComboBox + */ + private void loadTemplatesIntoComboBox(ComboBox templateComboBox) { + try { + List templates = taskTemplateService.findByUserId( + securityService.getCurrentDatabaseUser().getId() + ); + templateComboBox.setItems(templates); + } catch (Exception e) { + log.error("Error loading templates", e); + Notification.show("Fehler beim Laden der Templates: " + e.getMessage(), 4000, Notification.Position.MIDDLE); + } + } + + /** + * Loads tasks from selected template with confirmation dialog + */ + private void loadTasksFromTemplate(TaskTemplate template, ComboBox templateComboBox) { + ConfirmDialog confirmDialog = new ConfirmDialog(); + confirmDialog.setHeader("Template laden"); + confirmDialog.setText("Möchten Sie wirklich das Template '" + template.getTemplateName() + + "' laden? Alle aktuellen Aufgaben werden ersetzt."); + confirmDialog.setCancelable(true); + confirmDialog.setCancelText("Abbrechen"); + confirmDialog.setConfirmText("Laden"); + confirmDialog.setConfirmButtonTheme("primary"); + + confirmDialog.addConfirmListener(e -> { + try { + // Clear current tasks + tasksState.clear(); + tasksList.removeAll(); + + // Add tasks from template + if (template.getTasks() != null) { + for (BaseTask templateTask : template.getTasks()) { + BaseTask taskCopy = createTaskCopy(templateTask); + if (taskCopy != null) { + tasksState.add(taskCopy); + createTaskRowFromTask(taskCopy); + } + } + } + + // Clear the combobox selection + templateComboBox.clear(); + + Notification.show("Template '" + template.getTemplateName() + "' erfolgreich geladen", + 3000, Notification.Position.BOTTOM_END); + + } catch (Exception ex) { + log.error("Error loading template tasks", ex); + Notification.show("Fehler beim Laden des Templates: " + ex.getMessage(), + 4000, Notification.Position.MIDDLE); + } + }); + + confirmDialog.addCancelListener(e -> { + // Clear the combobox selection if user cancels + templateComboBox.clear(); + }); + + confirmDialog.open(); + } + + /** + * Creates a task row from an existing task (used when loading templates) + * This creates a UI row and populates it with the task's specific data + */ + private void createTaskRowFromTask(BaseTask task) { + // Don't call createTaskRow() directly, as it would create a default ConfirmationTask + // Instead, create the UI components and set them up with the loaded task + + VerticalLayout taskContainer = new VerticalLayout(); + taskContainer.setPadding(true); + taskContainer.setSpacing(true); + taskContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)"); + taskContainer.getStyle().set("border-radius", "var(--lumo-border-radius-m)"); + taskContainer.getStyle().set("background-color", "var(--lumo-base-color)"); + taskContainer.getStyle().set("position", "relative"); + + // Task type selection + ComboBox taskTypeCombo = new ComboBox<>("Aufgabentyp"); + taskTypeCombo.setItems(TaskType.values()); + taskTypeCombo.setItemLabelGenerator(TaskType::getDisplayName); + taskTypeCombo.setPlaceholder("Aufgabentyp wählen..."); + taskTypeCombo.setWidthFull(); + + // Configuration container for dynamic fields + VerticalLayout configContainer = new VerticalLayout(); + configContainer.setPadding(false); + configContainer.setSpacing(true); + + // Red X button positioned in top-right corner + Button deleteXButton = new Button(new Icon(VaadinIcon.CLOSE_SMALL)); + deleteXButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY); + deleteXButton.getStyle().set("position", "absolute"); + deleteXButton.getStyle().set("top", "8px"); + deleteXButton.getStyle().set("right", "8px"); + deleteXButton.getStyle().set("z-index", "10"); + deleteXButton.getStyle().set("padding", "4px"); + deleteXButton.getStyle().set("min-width", "24px"); + deleteXButton.getStyle().set("min-height", "24px"); + deleteXButton.addClickListener(e -> { + int idx = tasksList.getChildren().toList().indexOf(taskContainer); + if (idx >= 0 && idx < tasksState.size()) { + tasksState.remove(idx); + reorderTasksAfterDeletion(); + } + tasksList.remove(taskContainer); + }); + + taskContainer.add(taskTypeCombo, configContainer); + taskContainer.add(deleteXButton); + + // The task is already in tasksState from loadTasksFromTemplate + // Find the index and use it for the UI + int taskIndex = tasksState.size() - 1; // This should be the last added task + final BaseTask[] currentTask = { task }; + + // Set up the value change listener for the combo box + taskTypeCombo.addValueChangeListener(ev -> { + TaskType selectedType = ev.getValue(); + if (selectedType != null) { + BaseTask newTask = createTaskByType(selectedType); + BaseTask oldTask = currentTask[0]; + + newTask.setCompleted(oldTask.isCompleted()); + newTask.setCompletedAt(oldTask.getCompletedAt()); + newTask.setCompletedBy(oldTask.getCompletedBy()); + + // Preserve task-specific properties + switch (oldTask) { + case ConfirmationTask oldConfirmationTask when newTask instanceof ConfirmationTask newConfirmationTask -> + newConfirmationTask.setButtonText(oldConfirmationTask.getButtonText()); + case TodoListTask oldTodoTask when newTask instanceof TodoListTask newTodoTask -> + newTodoTask.setTodoItems(oldTodoTask.getTodoItems()); + case PhotoTask oldPhotoTask when newTask instanceof PhotoTask newPhotoTask -> { + newPhotoTask.setMinPhotoCount(oldPhotoTask.getMinPhotoCount()); + newPhotoTask.setMaxPhotoCount(oldPhotoTask.getMaxPhotoCount()); + } + case BarcodeTask oldBarcodeTask when newTask instanceof BarcodeTask newBarcodeTask -> { + newBarcodeTask.setMinBarcodeCount(oldBarcodeTask.getMinBarcodeCount()); + newBarcodeTask.setMaxBarcodeCount(oldBarcodeTask.getMaxBarcodeCount()); + } + case CommentTask oldCommentTask when newTask instanceof CommentTask newCommentTask -> { + newCommentTask.setCommentText(oldCommentTask.getCommentText()); + newCommentTask.setRequired(oldCommentTask.isRequired()); + } + default -> {} + } + + // Replace in state and preserve order + int index = tasksState.indexOf(oldTask); + if (index >= 0) { + newTask.setTaskOrder(index); + tasksState.set(index, newTask); + currentTask[0] = newTask; + } + + updateTaskConfiguration(configContainer, newTask); + } + }); + + // Set the correct task type based on the loaded task + TaskType taskType = getTaskTypeFromTask(task); + if (taskType != null) { + taskTypeCombo.setValue(taskType); + updateTaskConfiguration(configContainer, task); + } + + tasksList.add(taskContainer); + } + + /** + * Gets the TaskType enum value from a BaseTask instance + */ + private TaskType getTaskTypeFromTask(BaseTask task) { + if (task instanceof ConfirmationTask) return TaskType.CONFIRMATION; + if (task instanceof SignatureTask) return TaskType.SIGNATURE; + if (task instanceof TodoListTask) return TaskType.TODOLIST; + if (task instanceof PhotoTask) return TaskType.PHOTO; + if (task instanceof BarcodeTask) return TaskType.BARCODE; + if (task instanceof CommentTask) return TaskType.COMMENT; + return TaskType.CONFIRMATION; // fallback + } + + } diff --git a/src/main/java/de/assecutor/votianlt/repository/TaskTemplateRepository.java b/src/main/java/de/assecutor/votianlt/repository/TaskTemplateRepository.java new file mode 100644 index 0000000..89da446 --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/repository/TaskTemplateRepository.java @@ -0,0 +1,19 @@ +package de.assecutor.votianlt.repository; + +import de.assecutor.votianlt.model.TaskTemplate; +import org.bson.types.ObjectId; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface TaskTemplateRepository extends MongoRepository { + + List findByUserIdOrderByTemplateNameAsc(ObjectId userId); + + Optional findByUserIdAndTemplateName(ObjectId userId, String templateName); + + void deleteByUserIdAndTemplateName(ObjectId userId, String templateName); +} \ No newline at end of file