diff --git a/src/main/java/de/assecutor/votianlt/controller/MessageController.java b/src/main/java/de/assecutor/votianlt/controller/MessageController.java index 6dcad17..5fcb659 100644 --- a/src/main/java/de/assecutor/votianlt/controller/MessageController.java +++ b/src/main/java/de/assecutor/votianlt/controller/MessageController.java @@ -6,7 +6,7 @@ import de.assecutor.votianlt.dto.JobWithRelatedDataDTO; import de.assecutor.votianlt.model.AppUser; import de.assecutor.votianlt.model.CargoItem; import de.assecutor.votianlt.model.Job; -import de.assecutor.votianlt.model.TaskEntry; +import de.assecutor.votianlt.model.task.BaseTask; import de.assecutor.votianlt.pages.service.AppUserService; import de.assecutor.votianlt.repository.AppUserRepository; import de.assecutor.votianlt.repository.CargoItemRepository; @@ -140,7 +140,7 @@ public class MessageController { if (request == null || request.getEmail() == null || request.getPassword() == null || request.getEmail().isBlank() || request.getPassword().isBlank()) { - AppLoginResponse response = new AppLoginResponse(false, "E-Mail und Passwort sind erforderlich", null); + AppLoginResponse response = new AppLoginResponse(false, "E-Mail und Passwort sind erforderlich", null, null, null); log.info("STOMP Response for '/app/auth/login' sent to '/user/queue/auth': success={}, message='{}'", false, "E-Mail und Passwort sind erforderlich"); return response; @@ -148,7 +148,7 @@ public class MessageController { AppUser user = appUserRepository.findByEmail(request.getEmail()); if (user == null) { - AppLoginResponse response = new AppLoginResponse(false, "Benutzer nicht gefunden", null); + AppLoginResponse response = new AppLoginResponse(false, "Benutzer nicht gefunden", null, null, null); log.info("STOMP Response for '/app/auth/login' sent to '/user/queue/auth': success={}, message='{}'", false, "Benutzer nicht gefunden"); return response; @@ -156,13 +156,13 @@ public class MessageController { boolean ok = appUserService.verifyPassword(request.getPassword(), user.getPassword()); if (!ok) { - AppLoginResponse response = new AppLoginResponse(false, "Ungültige Anmeldedaten", null); + AppLoginResponse response = new AppLoginResponse(false, "Ungültige Anmeldedaten", null, null, null); log.info("STOMP Response for '/app/auth/login' sent to '/user/queue/auth': success={}, message='{}'", false, "Ungültige Anmeldedaten"); return response; } - AppLoginResponse response = new AppLoginResponse(true, "Anmeldung erfolgreich", user.getIdAsString()); + AppLoginResponse response = new AppLoginResponse(true, "Anmeldung erfolgreich", null, null, user.getIdAsString()); log.info("STOMP Response for '/app/auth/login' sent to '/user/queue/auth': success={}, message='{}', appUserId='{}'", true, "Anmeldung erfolgreich", response.getAppUserId()); return response; @@ -192,11 +192,18 @@ public class MessageController { // Find jobs assigned to this app user List assignedJobs = jobRepository.findByAppUser(appUserId); - // For each job, fetch related cargo items and tasks + // For each job, fetch related cargo items and tasks (ordered by task order) List jobsWithRelatedData = assignedJobs.stream() .map(job -> { List cargoItems = cargoItemRepository.findByJobId(job.getId()); - List tasks = taskRepository.findByJobId(job.getId()); + List tasks = taskRepository.findByJobIdOrderByTaskOrderAsc(job.getId()); + + // Log task details for debugging + tasks.forEach(task -> { + log.info("Task details for job {}: type={}, order={}", + job.getId(), task.getTaskType(), task.getTaskOrder()); + }); + return new JobWithRelatedDataDTO(job, cargoItems, tasks); }) .toList(); @@ -233,13 +240,13 @@ public class MessageController { try { org.bson.types.ObjectId taskId = new org.bson.types.ObjectId(taskIdStr); - java.util.Optional opt = taskRepository.findById(taskId); + java.util.Optional opt = taskRepository.findById(taskId); if (opt.isEmpty()) { response.put("success", false); response.put("message", "Task nicht gefunden"); return response; } - TaskEntry task = opt.get(); + BaseTask task = opt.get(); task.setCompleted(true); task.setCompletedAt(LocalDateTime.now()); if (completedBy != null) task.setCompletedBy(completedBy); diff --git a/src/main/java/de/assecutor/votianlt/dto/AppLoginRequest.java b/src/main/java/de/assecutor/votianlt/dto/AppLoginRequest.java index b287166..a98d0f4 100644 --- a/src/main/java/de/assecutor/votianlt/dto/AppLoginRequest.java +++ b/src/main/java/de/assecutor/votianlt/dto/AppLoginRequest.java @@ -1,9 +1,13 @@ package de.assecutor.votianlt.dto; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; @Data +@NoArgsConstructor +@AllArgsConstructor public class AppLoginRequest { private String email; private String password; -} +} \ No newline at end of file diff --git a/src/main/java/de/assecutor/votianlt/dto/AppLoginResponse.java b/src/main/java/de/assecutor/votianlt/dto/AppLoginResponse.java index 0691612..aeb0394 100644 --- a/src/main/java/de/assecutor/votianlt/dto/AppLoginResponse.java +++ b/src/main/java/de/assecutor/votianlt/dto/AppLoginResponse.java @@ -2,11 +2,15 @@ package de.assecutor.votianlt.dto; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; @Data +@NoArgsConstructor @AllArgsConstructor public class AppLoginResponse { private boolean success; private String message; - private String appUserId; // MongoDB ObjectId as hex string -} + private String token; + private String userId; + private String appUserId; +} \ No newline at end of file diff --git a/src/main/java/de/assecutor/votianlt/dto/JobWithRelatedDataDTO.java b/src/main/java/de/assecutor/votianlt/dto/JobWithRelatedDataDTO.java index ee2fc6a..7489702 100644 --- a/src/main/java/de/assecutor/votianlt/dto/JobWithRelatedDataDTO.java +++ b/src/main/java/de/assecutor/votianlt/dto/JobWithRelatedDataDTO.java @@ -2,7 +2,7 @@ package de.assecutor.votianlt.dto; import de.assecutor.votianlt.model.CargoItem; import de.assecutor.votianlt.model.Job; -import de.assecutor.votianlt.model.TaskEntry; +import de.assecutor.votianlt.model.task.BaseTask; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -19,5 +19,5 @@ import java.util.List; public class JobWithRelatedDataDTO { private Job job; private List cargoItems; - private List tasks; + private List tasks; } \ No newline at end of file diff --git a/src/main/java/de/assecutor/votianlt/model/task/BarcodeTask.java b/src/main/java/de/assecutor/votianlt/model/task/BarcodeTask.java new file mode 100644 index 0000000..9e3805b --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/model/task/BarcodeTask.java @@ -0,0 +1,33 @@ +package de.assecutor.votianlt.model.task; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.springframework.data.mongodb.core.mapping.Field; + +@Data +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class BarcodeTask extends BaseTask { + + @Field("min_barcode_count") + private Integer minBarcodeCount; + + @Field("max_barcode_count") + private Integer maxBarcodeCount; + + public BarcodeTask(Integer minBarcodeCount, Integer maxBarcodeCount) { + this.minBarcodeCount = minBarcodeCount; + this.maxBarcodeCount = maxBarcodeCount; + } + + @Override + public String getTaskType() { + return "BARCODE"; + } + + @Override + public String getDisplayName() { + return "Barcode"; + } +} diff --git a/src/main/java/de/assecutor/votianlt/model/task/BaseTask.java b/src/main/java/de/assecutor/votianlt/model/task/BaseTask.java new file mode 100644 index 0000000..2f27603 --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/model/task/BaseTask.java @@ -0,0 +1,80 @@ +package de.assecutor.votianlt.model.task; + +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import lombok.Data; +import lombok.NoArgsConstructor; +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.mapping.Field; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@Document(collection = "tasks") +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "taskType") +@JsonSubTypes({ + @JsonSubTypes.Type(value = ConfirmationTask.class, name = "CONFIRMATION"), + @JsonSubTypes.Type(value = SignatureTask.class, name = "SIGNATURE"), + @JsonSubTypes.Type(value = TodoListTask.class, name = "TODOLIST"), + @JsonSubTypes.Type(value = PhotoTask.class, name = "PHOTO"), + @JsonSubTypes.Type(value = BarcodeTask.class, name = "BARCODE") +}) +public abstract class BaseTask { + @Id + @JsonIgnore + private ObjectId id; + + @Field("job_id") + @JsonIgnore + private ObjectId jobId; + + @Field("text") + private String text; + + @Field("task_order") + private Integer taskOrder = 0; + + @Field("completed") + private boolean completed = false; + + @Field("completed_at") + private LocalDateTime completedAt; + + @Field("completed_by") + private String completedBy; + + @Field("completion_note") + private String completionNote; + + /** + * Returns the ObjectId as string for JSON serialization. + */ + @JsonGetter("id") + public String getIdAsString() { + return id != null ? id.toString() : null; + } + + /** + * Returns the job ObjectId as string for JSON serialization. + */ + @JsonGetter("jobId") + public String getJobIdAsString() { + return jobId != null ? jobId.toString() : null; + } + + /** + * Returns the task type as string for JSON serialization. + */ + @JsonGetter("taskType") + public abstract String getTaskType(); + + /** + * Returns the display name for this task type. + */ + public abstract String getDisplayName(); +} diff --git a/src/main/java/de/assecutor/votianlt/model/task/ConfirmationTask.java b/src/main/java/de/assecutor/votianlt/model/task/ConfirmationTask.java new file mode 100644 index 0000000..ba230b0 --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/model/task/ConfirmationTask.java @@ -0,0 +1,29 @@ +package de.assecutor.votianlt.model.task; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.springframework.data.mongodb.core.mapping.Field; + +@Data +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class ConfirmationTask extends BaseTask { + + @Field("button_text") + private String buttonText; + + public ConfirmationTask(String buttonText) { + this.buttonText = buttonText; + } + + @Override + public String getTaskType() { + return "CONFIRMATION"; + } + + @Override + public String getDisplayName() { + return "Bestätigung"; + } +} diff --git a/src/main/java/de/assecutor/votianlt/model/task/PhotoTask.java b/src/main/java/de/assecutor/votianlt/model/task/PhotoTask.java new file mode 100644 index 0000000..1cae814 --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/model/task/PhotoTask.java @@ -0,0 +1,33 @@ +package de.assecutor.votianlt.model.task; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.springframework.data.mongodb.core.mapping.Field; + +@Data +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class PhotoTask extends BaseTask { + + @Field("min_photo_count") + private Integer minPhotoCount; + + @Field("max_photo_count") + private Integer maxPhotoCount; + + public PhotoTask(Integer minPhotoCount, Integer maxPhotoCount) { + this.minPhotoCount = minPhotoCount; + this.maxPhotoCount = maxPhotoCount; + } + + @Override + public String getTaskType() { + return "PHOTO"; + } + + @Override + public String getDisplayName() { + return "Foto"; + } +} diff --git a/src/main/java/de/assecutor/votianlt/model/task/SignatureTask.java b/src/main/java/de/assecutor/votianlt/model/task/SignatureTask.java new file mode 100644 index 0000000..a5619de --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/model/task/SignatureTask.java @@ -0,0 +1,22 @@ +package de.assecutor.votianlt.model.task; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class SignatureTask extends BaseTask { + + + @Override + public String getTaskType() { + return "SIGNATURE"; + } + + @Override + public String getDisplayName() { + return "Unterschrift"; + } +} diff --git a/src/main/java/de/assecutor/votianlt/model/task/TaskType.java b/src/main/java/de/assecutor/votianlt/model/task/TaskType.java new file mode 100644 index 0000000..33eaa87 --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/model/task/TaskType.java @@ -0,0 +1,19 @@ +package de.assecutor.votianlt.model.task; + +public enum TaskType { + CONFIRMATION("Bestätigung"), + SIGNATURE("Unterschrift"), + TODOLIST("To-Do Liste"), + PHOTO("Foto"), + BARCODE("Barcode"); + + private final String displayName; + + TaskType(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } +} diff --git a/src/main/java/de/assecutor/votianlt/model/task/TodoListTask.java b/src/main/java/de/assecutor/votianlt/model/task/TodoListTask.java new file mode 100644 index 0000000..c7becf4 --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/model/task/TodoListTask.java @@ -0,0 +1,31 @@ +package de.assecutor.votianlt.model.task; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.springframework.data.mongodb.core.mapping.Field; + +import java.util.List; + +@Data +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class TodoListTask extends BaseTask { + + @Field("todo_items") + private List todoItems; + + public TodoListTask(List todoItems) { + this.todoItems = todoItems; + } + + @Override + public String getTaskType() { + return "TODOLIST"; + } + + @Override + public String getDisplayName() { + return "To-Do Liste"; + } +} diff --git a/src/main/java/de/assecutor/votianlt/pages/service/AddJobService.java b/src/main/java/de/assecutor/votianlt/pages/service/AddJobService.java index a489f5f..0324b26 100644 --- a/src/main/java/de/assecutor/votianlt/pages/service/AddJobService.java +++ b/src/main/java/de/assecutor/votianlt/pages/service/AddJobService.java @@ -3,7 +3,7 @@ package de.assecutor.votianlt.pages.service; import de.assecutor.votianlt.model.CargoItem; import de.assecutor.votianlt.model.Job; import de.assecutor.votianlt.model.JobStatus; -import de.assecutor.votianlt.model.TaskEntry; +import de.assecutor.votianlt.model.task.BaseTask; import de.assecutor.votianlt.repository.JobRepository; import de.assecutor.votianlt.repository.TaskRepository; import de.assecutor.votianlt.security.SecurityService; @@ -33,7 +33,7 @@ public class AddJobService { * @param job der Auftrag * @param transientCargo zugehörige, noch nicht gespeicherte CargoItems aus der View */ - public Job addJobWithCargo(Job job, List transientCargo, List transientTasks) { + public Job addJobWithCargo(Job job, List transientCargo, List transientTasks) { try { // Metadaten setzen LocalDateTime now = LocalDateTime.now(); @@ -70,15 +70,27 @@ public class AddJobService { }).toList(); cargoItemRepository.saveAll(itemsWithJob); modified = true; - // Tasks separat speichern und referenzieren - if (transientTasks != null && !transientTasks.isEmpty()) { - var prepared = transientTasks.stream() - .filter(Objects::nonNull) - .filter(te -> te.getText() != null && !te.getText().isBlank()) - .peek(te -> te.setJobId(jobId)) - .toList(); - taskRepository.saveAll(prepared); } + + // Tasks separat speichern und referenzieren mit korrekter Nummerierung + if (transientTasks != null && !transientTasks.isEmpty()) { + var filteredTasks = transientTasks.stream() + .filter(Objects::nonNull) + .filter(task -> task.getTaskType() != null) // Filter nach TaskType statt Text + .toList(); + + // Setze JobId und stelle sicher, dass taskOrder korrekt ist + for (int i = 0; i < filteredTasks.size(); i++) { + BaseTask task = filteredTasks.get(i); + task.setJobId(jobId); + // Verwende die bereits gesetzte taskOrder oder setze sie auf den Index + if (task.getTaskOrder() == null || task.getTaskOrder() != i) { + task.setTaskOrder(i); // Stelle sicher, dass die Reihenfolge stimmt + } + } + + taskRepository.saveAll(filteredTasks); + log.info("Saved {} tasks for job {} with ordering", filteredTasks.size(), jobId); } if (modified) { 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 40a295d..12f1754 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java @@ -28,7 +28,13 @@ import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.Route; import com.vaadin.flow.theme.lumo.LumoUtility; import de.assecutor.votianlt.model.Job; -import de.assecutor.votianlt.model.TaskEntry; +import de.assecutor.votianlt.model.task.BaseTask; +import de.assecutor.votianlt.model.task.TaskType; +import de.assecutor.votianlt.model.task.ConfirmationTask; +import de.assecutor.votianlt.model.task.SignatureTask; +import de.assecutor.votianlt.model.task.TodoListTask; +import de.assecutor.votianlt.model.task.PhotoTask; +import de.assecutor.votianlt.model.task.BarcodeTask; import de.assecutor.votianlt.pages.service.AddJobService; import de.assecutor.votianlt.pages.service.CustomerService; import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar; @@ -110,7 +116,7 @@ public class AddJobView extends Main { private final List cargoItemsState = new ArrayList<>(); // Stage sections for drag and drop // Backing list for tasks to mirror UI rows - private final List tasksState = new ArrayList<>(); + private final List tasksState = new ArrayList<>(); // Dynamic lists and additional controls // Cargo section UI refs for error highlighting private VerticalLayout cargoAreaContainer; @@ -1464,9 +1470,9 @@ public class AddJobView extends Main { // Task type selection - ComboBox taskTypeCombo = new ComboBox<>("Aufgabentyp"); - taskTypeCombo.setItems(TaskEntry.TaskType.values()); - taskTypeCombo.setItemLabelGenerator(TaskEntry.TaskType::getDisplayName); + ComboBox taskTypeCombo = new ComboBox<>("Aufgabentyp"); + taskTypeCombo.setItems(TaskType.values()); + taskTypeCombo.setItemLabelGenerator(TaskType::getDisplayName); taskTypeCombo.setPlaceholder("Aufgabentyp wählen..."); taskTypeCombo.setWidthFull(); @@ -1487,55 +1493,87 @@ public class AddJobView extends Main { 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); + if (idx >= 0 && idx < tasksState.size()) { + tasksState.remove(idx); + // Reorder remaining tasks to maintain correct sequence + reorderTasksAfterDeletion(); + } tasksList.remove(taskContainer); }); taskContainer.add(taskTypeCombo, configContainer); taskContainer.add(deleteXButton); - // Create TaskEntry and add to state - TaskEntry taskEntry = new TaskEntry(); - taskEntry.setText(""); - taskEntry.setTaskType(TaskEntry.TaskType.CONFIRMATION); - taskEntry.setConfiguration(new TaskEntry.TaskConfiguration()); - tasksState.add(taskEntry); + // Create TaskEntry and add to state with correct order + BaseTask task = new ConfirmationTask(""); + task.setTaskOrder(tasksState.size()); // Set order based on current position + tasksState.add(task); taskTypeCombo.addValueChangeListener(ev -> { - TaskEntry.TaskType selectedType = ev.getValue(); + TaskType selectedType = ev.getValue(); if (selectedType != null) { - taskEntry.setTaskType(selectedType); - updateTaskConfiguration(configContainer, taskEntry); + // Create new task instance based on type + BaseTask newTask = createTaskByType(selectedType); + newTask.setText(task.getText()); + newTask.setCompleted(task.isCompleted()); + newTask.setCompletedAt(task.getCompletedAt()); + newTask.setCompletedBy(task.getCompletedBy()); + newTask.setCompletionNote(task.getCompletionNote()); + + // Replace in state and preserve order + int index = tasksState.indexOf(task); + if (index >= 0) { + newTask.setTaskOrder(index); // Preserve the order + tasksState.set(index, newTask); + } + + updateTaskConfiguration(configContainer, newTask); } }); // Set initial configuration - taskTypeCombo.setValue(TaskEntry.TaskType.CONFIRMATION); - updateTaskConfiguration(configContainer, taskEntry); + taskTypeCombo.setValue(TaskType.CONFIRMATION); + updateTaskConfiguration(configContainer, task); tasksList.add(taskContainer); } - private void updateTaskConfiguration(VerticalLayout configContainer, TaskEntry taskEntry) { + private BaseTask createTaskByType(TaskType taskType) { + return switch (taskType) { + case CONFIRMATION -> new ConfirmationTask(""); + case SIGNATURE -> new SignatureTask(); + case TODOLIST -> new TodoListTask(new ArrayList<>()); + case PHOTO -> new PhotoTask(1, 10); + case BARCODE -> new BarcodeTask(1, 10); + }; + } + + private void reorderTasksAfterDeletion() { + // Reorder all tasks in tasksState to maintain correct sequence + for (int i = 0; i < tasksState.size(); i++) { + BaseTask task = tasksState.get(i); + if (task != null) { + task.setTaskOrder(i); // Reset order to match current position + } + } + } + + private void updateTaskConfiguration(VerticalLayout configContainer, BaseTask task) { configContainer.removeAll(); - TaskEntry.TaskType taskType = taskEntry.getTaskType(); + TaskType taskType = TaskType.valueOf(task.getTaskType()); if (taskType == null) return; - - // Ensure configuration is initialized - if (taskEntry.getConfiguration() == null) { - taskEntry.setConfiguration(new TaskEntry.TaskConfiguration()); - } switch (taskType) { case CONFIRMATION: TextField buttonTextField = new TextField("Button-Text"); buttonTextField.setPlaceholder("z.B. 'Bestätigen', 'Abgeschlossen'"); buttonTextField.setWidthFull(); - buttonTextField.setValue(taskEntry.getConfiguration().getButtonText() != null ? - taskEntry.getConfiguration().getButtonText() : ""); + ConfirmationTask confirmationTask = (ConfirmationTask) task; + buttonTextField.setValue(confirmationTask.getButtonText() != null ? + confirmationTask.getButtonText() : ""); buttonTextField.addValueChangeListener(ev -> { - taskEntry.getConfiguration().setButtonText(ev.getValue()); + confirmationTask.setButtonText(ev.getValue()); }); configContainer.add(buttonTextField); break; @@ -1575,14 +1613,14 @@ public class AddJobView extends Main { removeTodo.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY); removeTodo.addClickListener(e -> { todoList.remove(todoRow); - updateTodoItems(todoList, taskEntry); + updateTodoItems(todoList, task); }); todoRow.add(todoField, removeTodo); todoRow.setFlexGrow(1, todoField); todoList.add(todoRow); - todoField.addValueChangeListener(ev -> updateTodoItems(todoList, taskEntry)); + todoField.addValueChangeListener(ev -> updateTodoItems(todoList, task)); }; Button addTodoBtn = new Button("To-Do Punkt hinzufügen", new Icon(VaadinIcon.PLUS)); @@ -1601,26 +1639,27 @@ public class AddJobView extends Main { photoLayout.setWidthFull(); photoLayout.setSpacing(true); + PhotoTask photoTask = (PhotoTask) task; IntegerField minPhotos = new IntegerField("Min. Anzahl Fotos"); minPhotos.setPlaceholder("1"); minPhotos.setMin(1); - minPhotos.setValue(taskEntry.getConfiguration().getMinPhotoCount() != null ? - taskEntry.getConfiguration().getMinPhotoCount() : 1); + minPhotos.setValue(photoTask.getMinPhotoCount() != null ? + photoTask.getMinPhotoCount() : 1); IntegerField maxPhotos = new IntegerField("Max. Anzahl Fotos"); maxPhotos.setPlaceholder("10"); maxPhotos.setMin(1); - maxPhotos.setValue(taskEntry.getConfiguration().getMaxPhotoCount() != null ? - taskEntry.getConfiguration().getMaxPhotoCount() : 10); + maxPhotos.setValue(photoTask.getMaxPhotoCount() != null ? + photoTask.getMaxPhotoCount() : 10); photoLayout.add(minPhotos, maxPhotos); minPhotos.addValueChangeListener(ev -> { - taskEntry.getConfiguration().setMinPhotoCount(ev.getValue()); + photoTask.setMinPhotoCount(ev.getValue()); }); maxPhotos.addValueChangeListener(ev -> { - taskEntry.getConfiguration().setMaxPhotoCount(ev.getValue()); + photoTask.setMaxPhotoCount(ev.getValue()); }); configContainer.add(photoLayout); @@ -1631,26 +1670,27 @@ public class AddJobView extends Main { barcodeLayout.setWidthFull(); barcodeLayout.setSpacing(true); + BarcodeTask barcodeTask = (BarcodeTask) task; IntegerField minBarcodes = new IntegerField("Min. Anzahl Barcodes"); minBarcodes.setPlaceholder("1"); minBarcodes.setMin(1); - minBarcodes.setValue(taskEntry.getConfiguration().getMinBarcodeCount() != null ? - taskEntry.getConfiguration().getMinBarcodeCount() : 1); + minBarcodes.setValue(barcodeTask.getMinBarcodeCount() != null ? + barcodeTask.getMinBarcodeCount() : 1); IntegerField maxBarcodes = new IntegerField("Max. Anzahl Barcodes"); maxBarcodes.setPlaceholder("10"); maxBarcodes.setMin(1); - maxBarcodes.setValue(taskEntry.getConfiguration().getMaxBarcodeCount() != null ? - taskEntry.getConfiguration().getMaxBarcodeCount() : 10); + maxBarcodes.setValue(barcodeTask.getMaxBarcodeCount() != null ? + barcodeTask.getMaxBarcodeCount() : 10); barcodeLayout.add(minBarcodes, maxBarcodes); minBarcodes.addValueChangeListener(ev -> { - taskEntry.getConfiguration().setMinBarcodeCount(ev.getValue()); + barcodeTask.setMinBarcodeCount(ev.getValue()); }); maxBarcodes.addValueChangeListener(ev -> { - taskEntry.getConfiguration().setMaxBarcodeCount(ev.getValue()); + barcodeTask.setMaxBarcodeCount(ev.getValue()); }); configContainer.add(barcodeLayout); @@ -1658,7 +1698,7 @@ public class AddJobView extends Main { } } - private void updateTodoItems(VerticalLayout todoList, TaskEntry taskEntry) { + private void updateTodoItems(VerticalLayout todoList, BaseTask task) { List todoItems = todoList.getChildren() .map(component -> { if (component instanceof HorizontalLayout) { @@ -1672,7 +1712,9 @@ public class AddJobView extends Main { .filter(item -> !item.trim().isEmpty()) .collect(java.util.stream.Collectors.toList()); - taskEntry.getConfiguration().setTodoItems(todoItems); + if (task instanceof TodoListTask) { + ((TodoListTask) task).setTodoItems(todoItems); + } } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java b/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java index cacbb60..cc497d9 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java @@ -6,6 +6,12 @@ import com.vaadin.flow.component.html.Span; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.dialog.Dialog; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.html.H4; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.VaadinIcon; import com.vaadin.flow.router.BeforeEvent; import com.vaadin.flow.router.HasUrlParameter; import com.vaadin.flow.router.PageTitle; @@ -13,7 +19,11 @@ import com.vaadin.flow.router.Route; import com.vaadin.flow.theme.lumo.LumoUtility; import de.assecutor.votianlt.model.CargoItem; import de.assecutor.votianlt.model.Job; -import de.assecutor.votianlt.model.TaskEntry; +import de.assecutor.votianlt.model.task.BaseTask; +import de.assecutor.votianlt.model.task.TodoListTask; +import de.assecutor.votianlt.model.task.PhotoTask; +import de.assecutor.votianlt.model.task.SignatureTask; +import de.assecutor.votianlt.model.task.ConfirmationTask; import de.assecutor.votianlt.model.AppUser; import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar; import de.assecutor.votianlt.repository.CargoItemRepository; @@ -23,6 +33,7 @@ import de.assecutor.votianlt.pages.service.AppUserService; import jakarta.annotation.security.RolesAllowed; import org.bson.types.ObjectId; +import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Objects; @@ -38,6 +49,7 @@ public class JobSummaryView extends Main implements HasUrlParameter { private final AppUserService appUserService; private final VerticalLayout content; + private final List
taskCards = new ArrayList<>(); public JobSummaryView(JobRepository jobRepository, CargoItemRepository cargoItemRepository, @@ -70,12 +82,12 @@ public class JobSummaryView extends Main implements HasUrlParameter { Job job = jobRepository.findById(jobId).orElse(null); if (job == null) return; List cargo = cargoItemRepository.findByJobId(jobId); - List tasks = taskRepository.findByJobId(jobId); + List tasks = taskRepository.findByJobIdOrderByTaskOrderAsc(jobId); render(job, cargo, tasks); } - private void render(Job job, List cargoItems, List tasks) { + private void render(Job job, List cargoItems, List tasks) { content.removeAll(); // Kopfzeile: Abholung/Lieferung @@ -113,21 +125,28 @@ public class JobSummaryView extends Main implements HasUrlParameter { // Aufgaben VerticalLayout tasksBox = borderedBox(); tasksBox.add(new H3("Zu quittierende Aufgaben")); - if (tasks == null || tasks.stream().filter(Objects::nonNull).map(TaskEntry::getText).filter(t -> t != null && !t.isBlank()).findAny().isEmpty()) { + + // Ensure consistent spacing and width for task cards + tasksBox.setSpacing(false); + tasksBox.getStyle().set("gap", "var(--lumo-space-xs)"); + + // Clear previous task cards + taskCards.clear(); + + if (tasks == null || tasks.isEmpty()) { tasksBox.add(new Span("Keine Aufgaben")); } else { - tasks.stream() - .filter(Objects::nonNull) - .forEach(task -> { - String t = task.getText(); - if (t == null || t.isBlank()) return; - Span s = new Span("• " + t); - if (task.isCompleted()) { - // Use Lumo success color for completed tasks - s.getStyle().set("color", "var(--lumo-success-text-color)"); - } - tasksBox.add(s); - }); + for (BaseTask task : tasks) { + if (task != null) { + // Use getDisplayName() instead of getText() for task display + String displayName = task.getDisplayName(); + if (displayName != null && !displayName.isBlank()) { + Div taskCard = createTaskCard(task, displayName); + taskCards.add(taskCard); // Keep reference for hover reset + tasksBox.add(taskCard); + } + } + } } content.add(tasksBox); @@ -324,6 +343,253 @@ public class JobSummaryView extends Main implements HasUrlParameter { if (s == null) return ""; return s.replace("\\", "\\\\").replace("'", "\\'").replace("\n", " ").replace("\r", " "); } + + private void showTaskDetailsDialog(BaseTask task) { + Dialog dialog = new Dialog(); + dialog.setWidth("500px"); + dialog.setResizable(true); + dialog.setDraggable(true); + + // Reset all task card hover states when dialog closes + dialog.addDialogCloseActionListener(e -> resetAllTaskCardHoverStates()); + + VerticalLayout dialogContent = new VerticalLayout(); + dialogContent.setPadding(true); + dialogContent.setSpacing(true); + + // Header + H4 header = new H4("Aufgaben-Details"); + dialogContent.add(header); + + // Task type and status + Span typeSpan = new Span("Typ: " + task.getDisplayName()); + typeSpan.getStyle().set("font-weight", "bold"); + dialogContent.add(typeSpan); + + Span statusSpan = new Span("Status: " + (task.isCompleted() ? "Abgeschlossen" : "Offen")); + if (task.isCompleted()) { + statusSpan.getStyle().set("color", "var(--lumo-success-text-color)"); + } else { + statusSpan.getStyle().set("color", "var(--lumo-error-text-color)"); + } + dialogContent.add(statusSpan); + + // Task-specific details + addTaskSpecificDetails(dialogContent, task); + + // Completion details if completed + if (task.isCompleted()) { + dialogContent.add(new Span("")); // Spacer + if (task.getCompletedAt() != null) { + dialogContent.add(new Span("Abgeschlossen am: " + formatDateTime(task.getCompletedAt()))); + } + if (task.getCompletedBy() != null && !task.getCompletedBy().isBlank()) { + dialogContent.add(new Span("Abgeschlossen von: " + task.getCompletedBy())); + } + if (task.getCompletionNote() != null && !task.getCompletionNote().isBlank()) { + dialogContent.add(new Span("Notiz: " + task.getCompletionNote())); + } + } + + // Close button + Button closeButton = new Button("Schließen", e -> { + dialog.close(); + resetAllTaskCardHoverStates(); + }); + closeButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + + HorizontalLayout buttonLayout = new HorizontalLayout(closeButton); + buttonLayout.setJustifyContentMode(HorizontalLayout.JustifyContentMode.END); + dialogContent.add(buttonLayout); + + dialog.add(dialogContent); + dialog.open(); + } + + private void addTaskSpecificDetails(VerticalLayout content, BaseTask task) { + if (task instanceof TodoListTask todoTask) { + content.add(new Span("To-Do Items:")); + if (todoTask.getTodoItems() != null && !todoTask.getTodoItems().isEmpty()) { + for (String item : todoTask.getTodoItems()) { + if (item != null && !item.isBlank()) { + Span itemSpan = new Span(" • " + item); + itemSpan.getStyle().set("margin-left", "20px"); + content.add(itemSpan); + } + } + } else { + content.add(new Span(" Keine Items definiert")); + } + } else if (task instanceof PhotoTask photoTask) { + if (photoTask.getMinPhotoCount() != null || photoTask.getMaxPhotoCount() != null) { + String photoInfo = "Fotos: "; + if (photoTask.getMinPhotoCount() != null && photoTask.getMaxPhotoCount() != null) { + photoInfo += photoTask.getMinPhotoCount() + " - " + photoTask.getMaxPhotoCount() + " Fotos erforderlich"; + } else if (photoTask.getMinPhotoCount() != null) { + photoInfo += "Mindestens " + photoTask.getMinPhotoCount() + " Fotos erforderlich"; + } else if (photoTask.getMaxPhotoCount() != null) { + photoInfo += "Maximal " + photoTask.getMaxPhotoCount() + " Fotos erlaubt"; + } + content.add(new Span(photoInfo)); + } + } else if (task instanceof ConfirmationTask confirmationTask) { + if (confirmationTask.getButtonText() != null && !confirmationTask.getButtonText().isBlank()) { + content.add(new Span("Button-Text: " + confirmationTask.getButtonText())); + } + } else if (task instanceof SignatureTask) { + content.add(new Span("Unterschrift erforderlich")); + } + } + + private String formatDateTime(java.time.LocalDateTime dateTime) { + try { + java.time.format.DateTimeFormatter fmt = java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm").withLocale(Locale.GERMANY); + return dateTime.format(fmt); + } catch (Exception e) { + return dateTime.toString(); + } + } + + private Div createTaskCard(BaseTask task, String displayName) { + Div taskCard = new Div(); + + // Card styling with fixed width + taskCard.getStyle() + .set("border", "1px solid var(--lumo-contrast-20pct)") + .set("border-radius", "var(--lumo-border-radius-m)") + .set("padding", "var(--lumo-space-m)") + .set("margin", "var(--lumo-space-xs) 0") + .set("background-color", "var(--lumo-base-color)") + .set("cursor", "pointer") + .set("transition", "all 0.2s ease") + .set("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)") + .set("display", "flex") + .set("align-items", "center") + .set("gap", "var(--lumo-space-m)") + .set("width", "100%") + .set("box-sizing", "border-box"); + + // Hover effects + taskCard.getElement().addEventListener("mouseenter", e -> { + taskCard.getStyle() + .set("transform", "translateY(-2px)") + .set("box-shadow", "0 4px 12px rgba(0, 0, 0, 0.15)") + .set("border-color", "var(--lumo-primary-color-50pct)"); + }); + + taskCard.getElement().addEventListener("mouseleave", e -> { + taskCard.getStyle() + .set("transform", "translateY(0)") + .set("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)") + .set("border-color", "var(--lumo-contrast-20pct)"); + }); + + // Task icon based on type + Icon taskIcon = getTaskIcon(task); + taskIcon.getStyle().set("color", task.isCompleted() ? "var(--lumo-success-color)" : "var(--lumo-primary-color)"); + + // Task content + VerticalLayout taskContent = new VerticalLayout(); + taskContent.setPadding(false); + taskContent.setSpacing(false); + taskContent.getStyle().set("flex-grow", "1"); + + // Task name with order number (display as 1-based instead of 0-based) + String taskNameWithOrder = (task.getTaskOrder() != null ? (task.getTaskOrder() + 1) + ". " : "") + displayName; + Span taskName = new Span(taskNameWithOrder); + taskName.getStyle() + .set("font-weight", "500") + .set("font-size", "var(--lumo-font-size-m)") + .set("color", task.isCompleted() ? "var(--lumo-success-text-color)" : "var(--lumo-body-text-color)"); + + // Task status/description + Span taskDescription = new Span(getTaskDescription(task)); + taskDescription.getStyle() + .set("font-size", "var(--lumo-font-size-s)") + .set("color", "var(--lumo-secondary-text-color)") + .set("margin-top", "var(--lumo-space-xs)"); + + taskContent.add(taskName, taskDescription); + + // Status indicator + Div statusIndicator = new Div(); + statusIndicator.getStyle() + .set("width", "8px") + .set("height", "8px") + .set("border-radius", "50%") + .set("background-color", task.isCompleted() ? "var(--lumo-success-color)" : "var(--lumo-error-color)"); + + taskCard.add(taskIcon, taskContent, statusIndicator); + + // Click handler with hover state reset + taskCard.addClickListener(event -> { + showTaskDetailsDialog(task); + // Reset hover state after dialog interaction + taskCard.getStyle() + .set("transform", "translateY(0)") + .set("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)") + .set("border-color", "var(--lumo-contrast-20pct)"); + }); + + return taskCard; + } + + private Icon getTaskIcon(BaseTask task) { + if (task instanceof TodoListTask) { + return new Icon(VaadinIcon.LIST); + } else if (task instanceof PhotoTask) { + return new Icon(VaadinIcon.CAMERA); + } else if (task instanceof SignatureTask) { + return new Icon(VaadinIcon.EDIT); + } else if (task instanceof ConfirmationTask) { + return new Icon(VaadinIcon.CHECK_CIRCLE); + } else { + return new Icon(VaadinIcon.TASKS); + } + } + + private String getTaskDescription(BaseTask task) { + if (task.isCompleted()) { + return "Abgeschlossen" + (task.getCompletedAt() != null ? " am " + formatLocalDate(task.getCompletedAt().toLocalDate()) : ""); + } + + if (task instanceof TodoListTask todoTask) { + int itemCount = todoTask.getTodoItems() != null ? todoTask.getTodoItems().size() : 0; + return itemCount + " Aufgabe" + (itemCount != 1 ? "n" : "") + " zu erledigen"; + } else if (task instanceof PhotoTask photoTask) { + if (photoTask.getMinPhotoCount() != null && photoTask.getMaxPhotoCount() != null) { + return photoTask.getMinPhotoCount() + "-" + photoTask.getMaxPhotoCount() + " Fotos erforderlich"; + } else if (photoTask.getMinPhotoCount() != null) { + return "Mind. " + photoTask.getMinPhotoCount() + " Foto" + (photoTask.getMinPhotoCount() != 1 ? "s" : ""); + } else if (photoTask.getMaxPhotoCount() != null) { + return "Max. " + photoTask.getMaxPhotoCount() + " Foto" + (photoTask.getMaxPhotoCount() != 1 ? "s" : ""); + } else { + return "Foto erforderlich"; + } + } else if (task instanceof SignatureTask) { + return "Unterschrift erforderlich"; + } else if (task instanceof ConfirmationTask confirmationTask) { + if (confirmationTask.getButtonText() != null && !confirmationTask.getButtonText().isBlank()) { + return "Bestätigung: " + confirmationTask.getButtonText(); + } else { + return "Bestätigung erforderlich"; + } + } + + return "Aufgabe offen"; + } + + private void resetAllTaskCardHoverStates() { + // Reset hover state for all task cards + for (Div taskCard : taskCards) { + if (taskCard != null) { + taskCard.getStyle() + .set("transform", "translateY(0)") + .set("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)") + .set("border-color", "var(--lumo-contrast-20pct)"); + } + } + } } diff --git a/src/main/java/de/assecutor/votianlt/repository/TaskRepository.java b/src/main/java/de/assecutor/votianlt/repository/TaskRepository.java index b8d3bad..339e8f0 100644 --- a/src/main/java/de/assecutor/votianlt/repository/TaskRepository.java +++ b/src/main/java/de/assecutor/votianlt/repository/TaskRepository.java @@ -1,12 +1,18 @@ package de.assecutor.votianlt.repository; -import de.assecutor.votianlt.model.TaskEntry; +import de.assecutor.votianlt.model.task.BaseTask; import org.bson.types.ObjectId; import org.springframework.data.mongodb.repository.MongoRepository; import java.util.List; -public interface TaskRepository extends MongoRepository { - List findByJobId(ObjectId jobId); +public interface TaskRepository extends MongoRepository { + List findByJobIdOrderByTaskOrderAsc(ObjectId jobId); + + // Deprecated - use findByJobIdOrderByTaskOrderAsc instead + @Deprecated + default List findByJobId(ObjectId jobId) { + return findByJobIdOrderByTaskOrderAsc(jobId); + } } diff --git a/src/test/java/JsonSerializationTest.java b/src/test/java/JsonSerializationTest.java deleted file mode 100644 index eb8496c..0000000 --- a/src/test/java/JsonSerializationTest.java +++ /dev/null @@ -1,60 +0,0 @@ -import com.fasterxml.jackson.databind.ObjectMapper; -import de.assecutor.votianlt.dto.JobWithRelatedDataDTO; -import de.assecutor.votianlt.model.CargoItem; -import de.assecutor.votianlt.model.Job; -import de.assecutor.votianlt.model.TaskEntry; -import org.bson.types.ObjectId; - -import java.util.List; - -/** - * Simple test to verify JSON serialization of ObjectIds as strings - */ -public class JsonSerializationTest { - - public static void main(String[] args) throws Exception { - System.out.println("[DEBUG_LOG] Testing Job ID serialization..."); - - // Create test data - Job job = new Job(); - job.setId(new ObjectId()); - job.setJobNumber("TEST-001"); - - CargoItem cargo = new CargoItem(); - cargo.setId(new ObjectId()); - cargo.setJobId(job.getId()); - cargo.setDescription("Test cargo"); - - TaskEntry task = new TaskEntry(); - task.setId(new ObjectId()); - task.setJobId(job.getId()); - task.setText("Test task"); - - JobWithRelatedDataDTO dto = new JobWithRelatedDataDTO(job, List.of(cargo), List.of(task)); - - // Serialize to JSON - ObjectMapper mapper = new ObjectMapper(); - String json = mapper.writeValueAsString(dto); - - System.out.println("[DEBUG_LOG] Serialized JSON: " + json); - - // Check if job ID is serialized as string - if (json.contains("\"id\":\"" + job.getId().toString() + "\"")) { - System.out.println("[DEBUG_LOG] ✓ Job ID correctly serialized as string"); - } else if (json.contains("\"id\":{")) { - System.out.println("[DEBUG_LOG] ✗ Job ID serialized as ObjectId object"); - } else { - System.out.println("[DEBUG_LOG] ? Job ID serialization format unclear"); - } - - // Test individual Job serialization - String jobJson = mapper.writeValueAsString(job); - System.out.println("[DEBUG_LOG] Individual Job JSON: " + jobJson); - - if (jobJson.contains("\"id\":\"" + job.getId().toString() + "\"")) { - System.out.println("[DEBUG_LOG] ✓ Individual Job ID correctly serialized as string"); - } else { - System.out.println("[DEBUG_LOG] ✗ Individual Job ID not serialized as string"); - } - } -} \ No newline at end of file