Erweiterungen

This commit is contained in:
2025-09-11 22:02:26 +02:00
parent 04fe67574b
commit 3e22a9c520
16 changed files with 674 additions and 146 deletions

View File

@@ -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<Job> 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<JobWithRelatedDataDTO> jobsWithRelatedData = assignedJobs.stream()
.map(job -> {
List<CargoItem> cargoItems = cargoItemRepository.findByJobId(job.getId());
List<TaskEntry> tasks = taskRepository.findByJobId(job.getId());
List<BaseTask> 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<TaskEntry> opt = taskRepository.findById(taskId);
java.util.Optional<BaseTask> 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);

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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<CargoItem> cargoItems;
private List<TaskEntry> tasks;
private List<BaseTask> tasks;
}

View File

@@ -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";
}
}

View File

@@ -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();
}

View File

@@ -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";
}
}

View File

@@ -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";
}
}

View File

@@ -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";
}
}

View File

@@ -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;
}
}

View File

@@ -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<String> todoItems;
public TodoListTask(List<String> todoItems) {
this.todoItems = todoItems;
}
@Override
public String getTaskType() {
return "TODOLIST";
}
@Override
public String getDisplayName() {
return "To-Do Liste";
}
}

View File

@@ -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<CargoItem> transientCargo, List<TaskEntry> transientTasks) {
public Job addJobWithCargo(Job job, List<CargoItem> transientCargo, List<BaseTask> 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) {

View File

@@ -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<CargoItem> cargoItemsState = new ArrayList<>();
// Stage sections for drag and drop
// Backing list for tasks to mirror UI rows
private final List<TaskEntry> tasksState = new ArrayList<>();
private final List<BaseTask> 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<TaskEntry.TaskType> taskTypeCombo = new ComboBox<>("Aufgabentyp");
taskTypeCombo.setItems(TaskEntry.TaskType.values());
taskTypeCombo.setItemLabelGenerator(TaskEntry.TaskType::getDisplayName);
ComboBox<TaskType> 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<String> 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);
}
}
}

View File

@@ -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<String> {
private final AppUserService appUserService;
private final VerticalLayout content;
private final List<Div> taskCards = new ArrayList<>();
public JobSummaryView(JobRepository jobRepository,
CargoItemRepository cargoItemRepository,
@@ -70,12 +82,12 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
Job job = jobRepository.findById(jobId).orElse(null);
if (job == null) return;
List<CargoItem> cargo = cargoItemRepository.findByJobId(jobId);
List<TaskEntry> tasks = taskRepository.findByJobId(jobId);
List<BaseTask> tasks = taskRepository.findByJobIdOrderByTaskOrderAsc(jobId);
render(job, cargo, tasks);
}
private void render(Job job, List<CargoItem> cargoItems, List<TaskEntry> tasks) {
private void render(Job job, List<CargoItem> cargoItems, List<BaseTask> tasks) {
content.removeAll();
// Kopfzeile: Abholung/Lieferung
@@ -113,21 +125,28 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
// 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<String> {
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)");
}
}
}
}

View File

@@ -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<TaskEntry, ObjectId> {
List<TaskEntry> findByJobId(ObjectId jobId);
public interface TaskRepository extends MongoRepository<BaseTask, ObjectId> {
List<BaseTask> findByJobIdOrderByTaskOrderAsc(ObjectId jobId);
// Deprecated - use findByJobIdOrderByTaskOrderAsc instead
@Deprecated
default List<BaseTask> findByJobId(ObjectId jobId) {
return findByJobIdOrderByTaskOrderAsc(jobId);
}
}