Erweiterungen

This commit is contained in:
2025-09-23 14:04:09 +02:00
parent d636c9e9d0
commit 435522cf7e
4 changed files with 512 additions and 5 deletions

View File

@@ -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<BaseTask> tasks;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
public TaskTemplate() {
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
public void updateTimestamp() {
this.updatedAt = LocalDateTime.now();
}
}

View File

@@ -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<TaskTemplate> findByUserId(ObjectId userId) {
return taskTemplateRepository.findByUserIdOrderByTemplateNameAsc(userId);
}
public Optional<TaskTemplate> 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<BaseTask> tasks) {
// Check if template with same name already exists for this user
Optional<TaskTemplate> 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();
}
}

View File

@@ -4,6 +4,8 @@ import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.checkbox.Checkbox; import com.vaadin.flow.component.checkbox.Checkbox;
import com.vaadin.flow.component.combobox.ComboBox; 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.datepicker.DatePicker;
import com.vaadin.flow.component.html.H2; 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.model.Customer;
import de.assecutor.votianlt.pages.service.AppUserService; import de.assecutor.votianlt.pages.service.AppUserService;
import de.assecutor.votianlt.model.AppUser; 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 jakarta.annotation.security.RolesAllowed;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -63,6 +68,8 @@ public class AddJobView extends Main {
private final CustomerService customerService; private final CustomerService customerService;
private final AddCustomerService addCustomerService; private final AddCustomerService addCustomerService;
private final AppUserService appUserService; private final AppUserService appUserService;
private final TaskTemplateService taskTemplateService;
private final SecurityService securityService;
// Customer selection // Customer selection
private ComboBox<String> customerSelection; private ComboBox<String> customerSelection;
@@ -138,11 +145,14 @@ public class AddJobView extends Main {
private List<AppUser> availableAppUsers; private List<AppUser> availableAppUsers;
public AddJobView(AddJobService addJobService, AddCustomerService addCustomerService, public AddJobView(AddJobService addJobService, AddCustomerService addCustomerService,
CustomerService customerService, AppUserService appUserService) { CustomerService customerService, AppUserService appUserService,
TaskTemplateService taskTemplateService, SecurityService securityService) {
this.addJobService = addJobService; this.addJobService = addJobService;
this.addCustomerService = addCustomerService; this.addCustomerService = addCustomerService;
this.customerService = customerService; this.customerService = customerService;
this.appUserService = appUserService; this.appUserService = appUserService;
this.taskTemplateService = taskTemplateService;
this.securityService = securityService;
initializeComponents(); initializeComponents();
setupLayout(); setupLayout();
setupValidation(); setupValidation();
@@ -1295,10 +1305,37 @@ public class AddJobView extends Main {
content.setSpacing(true); content.setSpacing(true);
content.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH); content.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH);
// Aufgabentitel // Aufgabentitel mit Template-Auswahl
H3 tasksTitle = new H3("Zu quittierende Aufgaben"); H3 tasksTitle = new H3("Zu quittierende Aufgaben");
tasksTitle.getStyle().set("margin", "0"); tasksTitle.getStyle().set("margin", "0");
content.add(tasksTitle);
ComboBox<TaskTemplate> 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 // Dynamische Aufgabenliste
tasksList = new VerticalLayout(); tasksList = new VerticalLayout();
@@ -1664,8 +1701,42 @@ public class AddJobView extends Main {
addTodoBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY); addTodoBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
addTodoBtn.addClickListener(e -> addTodoItem.accept(null)); addTodoBtn.addClickListener(e -> addTodoItem.accept(null));
// Add initial todo item // Load existing todo items from the task, or add one empty item if none exist
addTodoItem.accept(null); 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); todoContainer.add(todoList, addTodoBtn);
configContainer.add(todoContainer); 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<BaseTask> 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<TaskTemplate> templateComboBox) {
try {
List<TaskTemplate> 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<TaskTemplate> 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<TaskType> 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
}
} }

View File

@@ -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<TaskTemplate, ObjectId> {
List<TaskTemplate> findByUserIdOrderByTemplateNameAsc(ObjectId userId);
Optional<TaskTemplate> findByUserIdAndTemplateName(ObjectId userId, String templateName);
void deleteByUserIdAndTemplateName(ObjectId userId, String templateName);
}