Version 0.9.16: Skip-Button entfernt, manuelle Auftragsbeendigung und E-Mail-Verbesserungen
App: - Skip-Button für optionale Aufgaben entfernt — optionale Aufgaben blockieren nicht mehr den Fortschritt und können jederzeit nachträglich bearbeitet werden Backend: - Manuelle Auftragsbeendigung mit Begründung in der Job-Zusammenfassung hinzugefügt - Leere Lieferstationen werden beim Übernehmen automatisch entfernt - E-Mail-Benachrichtigungen zeigen jetzt den tatsächlichen App-Benutzernamen an - WebSocket: konfigurierbare Max-Nachrichtengröße und Session-Idle-Timeout - docker_push.sh Pfadkorrektur - Lokalisierungen für 10 Sprachen aktualisiert - EmailService-Test hinzugefügt Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -191,7 +191,7 @@
|
||||
},
|
||||
{
|
||||
"id": "5:2194624907249454848",
|
||||
"lastPropertyId": "6:5035828038544573244",
|
||||
"lastPropertyId": "7:5673785903451668117",
|
||||
"name": "TaskStatusEntity",
|
||||
"properties": [
|
||||
{
|
||||
@@ -275,7 +275,9 @@
|
||||
"modelVersionParserMinimum": 5,
|
||||
"retiredEntityUids": [],
|
||||
"retiredIndexUids": [],
|
||||
"retiredPropertyUids": [],
|
||||
"retiredPropertyUids": [
|
||||
5673785903451668117
|
||||
],
|
||||
"retiredRelationUids": [],
|
||||
"version": 1
|
||||
}
|
||||
@@ -240,7 +240,7 @@ final _entities = <obx_int.ModelEntity>[
|
||||
obx_int.ModelEntity(
|
||||
id: const obx_int.IdUid(5, 2194624907249454848),
|
||||
name: 'TaskStatusEntity',
|
||||
lastPropertyId: const obx_int.IdUid(6, 5035828038544573244),
|
||||
lastPropertyId: const obx_int.IdUid(7, 5673785903451668117),
|
||||
flags: 0,
|
||||
properties: <obx_int.ModelProperty>[
|
||||
obx_int.ModelProperty(
|
||||
@@ -371,7 +371,7 @@ obx_int.ModelDefinition getObjectBoxModel() {
|
||||
lastSequenceId: const obx_int.IdUid(0, 0),
|
||||
retiredEntityUids: const [],
|
||||
retiredIndexUids: const [],
|
||||
retiredPropertyUids: const [],
|
||||
retiredPropertyUids: const [5673785903451668117],
|
||||
retiredRelationUids: const [],
|
||||
modelVersion: 5,
|
||||
modelVersionParserMinimum: 5,
|
||||
@@ -632,7 +632,7 @@ obx_int.ModelDefinition getObjectBoxModel() {
|
||||
},
|
||||
objectToFB: (TaskStatusEntity object, fb.Builder fbb) {
|
||||
final taskIdOffset = fbb.writeString(object.taskId);
|
||||
fbb.startTable(7);
|
||||
fbb.startTable(8);
|
||||
fbb.addInt64(0, object.id);
|
||||
fbb.addOffset(1, taskIdOffset);
|
||||
fbb.addBool(2, object.completed);
|
||||
|
||||
@@ -40,7 +40,6 @@ class TaskView extends StatefulWidget {
|
||||
|
||||
class _TaskViewState extends State<TaskView> {
|
||||
final Set<String> _completedTasks = {};
|
||||
final Set<String> _skippedTasks = {};
|
||||
final DatabaseService _databaseService = DatabaseService();
|
||||
// Store SVG representations of signatures per task for later use
|
||||
final Map<String, String> _signatureSvgByTask = {};
|
||||
@@ -61,7 +60,7 @@ class _TaskViewState extends State<TaskView> {
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Load task completion statuses from database and merge with JSON task states
|
||||
/// Load task completion and skipped statuses from database and merge with JSON task states
|
||||
Future<void> _loadTaskStatuses() async {
|
||||
final statuses = await _databaseService.loadAllTaskStatuses();
|
||||
setState(() {
|
||||
@@ -170,33 +169,26 @@ class _TaskViewState extends State<TaskView> {
|
||||
itemBuilder: (context, index) {
|
||||
final task = _visibleTasks[index];
|
||||
final isCompleted = _completedTasks.contains(task.id);
|
||||
final isSkipped = _skippedTasks.contains(task.id);
|
||||
final canBeCompletedNow =
|
||||
!isCompleted && !isSkipped && _arePreviousTasksCompleted(index);
|
||||
!isCompleted && _arePreviousTasksCompleted(index);
|
||||
|
||||
// Hintergrundfarbe je nach Status:
|
||||
// abgeschlossen → hellgrün, übersprungen → hellgelb, bearbeitbar → weiß, gesperrt → hellgrau
|
||||
// abgeschlossen → hellgrün, bearbeitbar → weiß, gesperrt → hellgrau
|
||||
final Color cardColor =
|
||||
isCompleted
|
||||
? const Color(0xFFE8F5E9) // hellgrün
|
||||
: isSkipped
|
||||
? const Color(0xFFFFF8E1) // hellgelb
|
||||
: canBeCompletedNow
|
||||
? Colors.white
|
||||
: const Color(0xFFF5F5F5); // hellgrau
|
||||
final Color borderColor =
|
||||
isCompleted
|
||||
? Colors.green[300]!
|
||||
: isSkipped
|
||||
? Colors.amber[300]!
|
||||
: canBeCompletedNow
|
||||
? Colors.grey[300]!
|
||||
: Colors.grey[200]!;
|
||||
final Color circleColor =
|
||||
isCompleted
|
||||
? Colors.green[600]!
|
||||
: isSkipped
|
||||
? Colors.amber[600]!
|
||||
: canBeCompletedNow
|
||||
? Colors.deepPurple[400]!
|
||||
: Colors.grey[400]!;
|
||||
@@ -250,7 +242,7 @@ class _TaskViewState extends State<TaskView> {
|
||||
children: [
|
||||
_buildTaskDisplayText(
|
||||
task,
|
||||
isCompleted || isSkipped,
|
||||
isCompleted,
|
||||
index,
|
||||
),
|
||||
if (_getTaskStationLabel(task) != null) ...[
|
||||
@@ -264,6 +256,28 @@ class _TaskViewState extends State<TaskView> {
|
||||
),
|
||||
),
|
||||
],
|
||||
if (task.optional) ...[
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.amber[50],
|
||||
border: Border.all(color: Colors.amber[300]!),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).optional,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.amber[800],
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -271,10 +285,6 @@ class _TaskViewState extends State<TaskView> {
|
||||
const SizedBox(width: 8),
|
||||
Icon(Icons.check_circle, color: Colors.green[600]),
|
||||
],
|
||||
if (isSkipped) ...[
|
||||
const SizedBox(width: 8),
|
||||
Icon(Icons.skip_next, color: Colors.amber[600]),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -634,9 +644,7 @@ class _TaskViewState extends State<TaskView> {
|
||||
if (index <= 0) return true;
|
||||
for (int i = 0; i < index; i++) {
|
||||
final t = _visibleTasks[i];
|
||||
if (!t.optional &&
|
||||
!_completedTasks.contains(t.id) &&
|
||||
!_skippedTasks.contains(t.id)) {
|
||||
if (!t.optional && !_completedTasks.contains(t.id)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 0.9.15+1
|
||||
version: 0.9.16+1
|
||||
|
||||
environment:
|
||||
sdk: ^3.7.0
|
||||
|
||||
@@ -4,7 +4,7 @@ set -euo pipefail
|
||||
|
||||
readonly SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly REGISTRY_IMAGE="registry.assecutor.org/votianlt"
|
||||
readonly BACKEND_DIR="${SCRIPT_DIR}/backend"
|
||||
readonly BACKEND_DIR="${SCRIPT_DIR}"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<revision>0.9.15</revision>
|
||||
<revision>0.9.16</revision>
|
||||
<java.version>21</java.version>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package de.assecutor.votianlt.messaging;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocket;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
|
||||
import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;
|
||||
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
|
||||
|
||||
/**
|
||||
@@ -23,6 +25,12 @@ public class WebSocketConfig implements WebSocketConfigurer {
|
||||
@Value("${app.messaging.websocket.allowed-origins:*}")
|
||||
private String allowedOrigins;
|
||||
|
||||
@Value("${app.messaging.websocket.max-text-message-size:10485760}")
|
||||
private int maxTextMessageSize;
|
||||
|
||||
@Value("${app.messaging.websocket.max-session-idle-timeout:300000}")
|
||||
private long maxSessionIdleTimeout;
|
||||
|
||||
public WebSocketConfig(WebSocketService webSocketService) {
|
||||
this.webSocketService = webSocketService;
|
||||
}
|
||||
@@ -32,4 +40,13 @@ public class WebSocketConfig implements WebSocketConfigurer {
|
||||
registry.addHandler(webSocketService, wsPath).setAllowedOrigins(allowedOrigins.split(","))
|
||||
.addInterceptors(new HttpSessionHandshakeInterceptor());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ServletServerContainerFactoryBean createWebSocketContainer() {
|
||||
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
|
||||
container.setMaxTextMessageBufferSize(maxTextMessageSize);
|
||||
container.setMaxBinaryMessageBufferSize(maxTextMessageSize);
|
||||
container.setMaxSessionIdleTimeout(maxSessionIdleTimeout);
|
||||
return container;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2093,7 +2093,103 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt alle leeren (nicht editierten) Lieferstationen, sofern mindestens
|
||||
* eine valide Lieferstation übrig bleibt. Wird aufgerufen bevor die Stationen
|
||||
* übernommen werden.
|
||||
*/
|
||||
private void removeEmptyDeliveryStations() {
|
||||
// Indizes der leeren Stationen sammeln (absteigend, damit beim Entfernen die Indizes stabil bleiben)
|
||||
List<Integer> emptyIndices = new ArrayList<>();
|
||||
for (int i = 0; i < deliveryStationsState.size(); i++) {
|
||||
if (hasDeliveryStationValidationErrors(deliveryStationsState.get(i))) {
|
||||
emptyIndices.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Mindestens eine valide Station muss übrig bleiben
|
||||
if (emptyIndices.size() >= deliveryStationsState.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Von hinten nach vorne entfernen, damit Indizes stabil bleiben
|
||||
for (int k = emptyIndices.size() - 1; k >= 0; k--) {
|
||||
int idx = emptyIndices.get(k);
|
||||
|
||||
deliveryStationTilesList.remove(idx);
|
||||
deliveryStationsState.remove(idx);
|
||||
deliveryStationsSaveAddress.remove(idx);
|
||||
deliveryStationsMailState.remove(idx);
|
||||
deliveryStationsValidatedByGoogle.remove(idx);
|
||||
deliveryStationTasksState.remove(idx);
|
||||
Div removedSlot = deliveryStationSlotList.remove(idx);
|
||||
deliveryStationDistanceChips.remove(idx);
|
||||
pickupToDeliveryRouteResults.remove(idx);
|
||||
stationsGridContainer.remove(removedSlot);
|
||||
}
|
||||
|
||||
// Tasks und Routen-Maps re-indizieren
|
||||
Map<Integer, List<BaseTask>> reindexedTasks = new HashMap<>();
|
||||
for (Map.Entry<Integer, List<BaseTask>> entry : deliveryStationTasksState.entrySet()) {
|
||||
int oldIdx = entry.getKey();
|
||||
int newIdx = oldIdx - (int) emptyIndices.stream().filter(ei -> ei < oldIdx).count();
|
||||
reindexedTasks.put(newIdx, entry.getValue());
|
||||
}
|
||||
deliveryStationTasksState.clear();
|
||||
deliveryStationTasksState.putAll(reindexedTasks);
|
||||
|
||||
Map<Integer, RouteCalculationResult> reindexedRoutes = new HashMap<>();
|
||||
for (Map.Entry<Integer, RouteCalculationResult> entry : pickupToDeliveryRouteResults.entrySet()) {
|
||||
int oldIdx = entry.getKey();
|
||||
int newIdx = oldIdx - (int) emptyIndices.stream().filter(ei -> ei < oldIdx).count();
|
||||
reindexedRoutes.put(newIdx, entry.getValue());
|
||||
}
|
||||
pickupToDeliveryRouteResults.clear();
|
||||
pickupToDeliveryRouteResults.putAll(reindexedRoutes);
|
||||
|
||||
// Service-Zuordnungen anpassen
|
||||
for (SelectedServiceEntry selectedService : selectedServices) {
|
||||
Integer stationOrder = selectedService.getDeliveryStationOrder();
|
||||
if (stationOrder == null) {
|
||||
continue;
|
||||
}
|
||||
if (emptyIndices.contains(stationOrder)) {
|
||||
selectedService.setDeliveryStationOrder(deliveryStationsState.isEmpty() ? null : 0);
|
||||
} else {
|
||||
int newOrder = stationOrder - (int) emptyIndices.stream().filter(ei -> ei < stationOrder).count();
|
||||
selectedService.setDeliveryStationOrder(newOrder);
|
||||
}
|
||||
}
|
||||
|
||||
// Tiles neu nummerieren und Click-Listener aktualisieren
|
||||
for (int i = 0; i < deliveryStationTilesList.size(); i++) {
|
||||
StationTile t = deliveryStationTilesList.get(i);
|
||||
int newNumber = i + 1;
|
||||
t.updateStationNumber(newNumber);
|
||||
t.updateTitle(getTranslation("addjob.station.delivery", newNumber));
|
||||
final int newIdx = i;
|
||||
t.setClickListener(tt -> openDeliveryDialog(tt, newIdx));
|
||||
if (i == 0) {
|
||||
t.setDeleteListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
// "+" Button wieder anzeigen falls unter Maximum
|
||||
if (deliveryStationTilesList.size() < MAX_DELIVERY_STATIONS
|
||||
&& addStationButtonSlot.getParent().isEmpty()) {
|
||||
stationsGridContainer.add(addStationButtonSlot);
|
||||
}
|
||||
|
||||
if (servicesGrid != null) {
|
||||
servicesGrid.getDataProvider().refreshAll();
|
||||
}
|
||||
updatePriceSummary();
|
||||
triggerValidation();
|
||||
updateTabLabels();
|
||||
}
|
||||
|
||||
private void handleApplyStations() {
|
||||
removeEmptyDeliveryStations();
|
||||
revealPriceAndDetailsSection();
|
||||
|
||||
if (!areAllStationsValidatedByGoogle()) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.notification.NotificationVariant;
|
||||
import com.vaadin.flow.component.icon.Icon;
|
||||
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||
import com.vaadin.flow.component.textfield.TextArea;
|
||||
import com.vaadin.flow.component.AttachEvent;
|
||||
import com.vaadin.flow.component.DetachEvent;
|
||||
import com.vaadin.flow.component.UI;
|
||||
@@ -59,6 +60,8 @@ import de.assecutor.votianlt.service.JobUpdateBroadcaster;
|
||||
import de.assecutor.votianlt.service.LocationService;
|
||||
import de.assecutor.votianlt.service.MessageService;
|
||||
import de.assecutor.votianlt.service.TaskAssignmentService;
|
||||
import de.assecutor.votianlt.model.JobHistoryType;
|
||||
import de.assecutor.votianlt.security.SecurityService;
|
||||
import de.assecutor.votianlt.util.DateTimeFormatUtil;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import org.bson.types.ObjectId;
|
||||
@@ -88,6 +91,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
||||
private final LocationService locationService;
|
||||
private final ServiceRepository serviceRepository;
|
||||
private final TaskAssignmentService taskAssignmentService;
|
||||
private final SecurityService securityService;
|
||||
|
||||
@Value("${app.google.maps.api-key}")
|
||||
private String googleMapsApiKey;
|
||||
@@ -103,7 +107,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
||||
PhotoRepository photoRepository, CommentRepository commentRepository, AppUserService appUserService,
|
||||
MessageService messageService, JobHistoryService jobHistoryService,
|
||||
JobUpdateBroadcaster jobUpdateBroadcaster, LocationService locationService,
|
||||
ServiceRepository serviceRepository, TaskAssignmentService taskAssignmentService) {
|
||||
ServiceRepository serviceRepository, TaskAssignmentService taskAssignmentService,
|
||||
SecurityService securityService) {
|
||||
this.jobRepository = jobRepository;
|
||||
this.cargoItemRepository = cargoItemRepository;
|
||||
this.signatureRepository = signatureRepository;
|
||||
@@ -116,6 +121,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
||||
this.locationService = locationService;
|
||||
this.serviceRepository = serviceRepository;
|
||||
this.taskAssignmentService = taskAssignmentService;
|
||||
this.securityService = securityService;
|
||||
|
||||
setSizeFull();
|
||||
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN,
|
||||
@@ -212,6 +218,15 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
||||
getUI().ifPresent(ui -> ui.navigate("message-details/" + appUserId + "/" + conversationId));
|
||||
});
|
||||
|
||||
// Create Manual Completion Button for app jobs (digital processing)
|
||||
Button manualCompleteButton = null;
|
||||
if (job.isDigitalProcessing() && job.getStatus() != JobStatus.COMPLETED
|
||||
&& job.getStatus() != JobStatus.CANCELLED) {
|
||||
manualCompleteButton = new Button(getTranslation("jobsummary.button.manualcomplete"));
|
||||
manualCompleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR);
|
||||
manualCompleteButton.addClickListener(e -> openManualCompleteDialog(job));
|
||||
}
|
||||
|
||||
// Create Job History Button for toolbar
|
||||
Button jobHistoryButton = new Button(getTranslation("jobsummary.button.jobhistory"));
|
||||
jobHistoryButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
@@ -219,8 +234,13 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
||||
getUI().ifPresent(ui -> ui.navigate("job_history/" + job.getId().toHexString()));
|
||||
});
|
||||
|
||||
// Add toolbar with both buttons in top right (Send Message button on the left)
|
||||
add(new ViewToolbar(getTranslation("jobsummary.title"), sendMessageButton, jobHistoryButton));
|
||||
// Add toolbar with buttons
|
||||
if (manualCompleteButton != null) {
|
||||
add(new ViewToolbar(getTranslation("jobsummary.title"), manualCompleteButton, sendMessageButton,
|
||||
jobHistoryButton));
|
||||
} else {
|
||||
add(new ViewToolbar(getTranslation("jobsummary.title"), sendMessageButton, jobHistoryButton));
|
||||
}
|
||||
|
||||
List<CargoItem> cargo = cargoItemRepository.findByJobId(currentJobId);
|
||||
List<BaseTask> tasks = taskAssignmentService.findTasksForJob(job);
|
||||
@@ -359,6 +379,75 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
||||
}
|
||||
}
|
||||
|
||||
private void openManualCompleteDialog(Job job) {
|
||||
Dialog dialog = DialogStylingHelper.createStyledDialog(
|
||||
getTranslation("jobsummary.dialog.manualcomplete.title"), "560px");
|
||||
|
||||
VerticalLayout dialogContent = DialogStylingHelper.createContentLayout("520px");
|
||||
|
||||
Span warningText = new Span(getTranslation("jobsummary.dialog.manualcomplete.text", job.getJobNumber()));
|
||||
warningText.getStyle().set("color", "var(--lumo-error-text-color)");
|
||||
|
||||
TextArea reasonField = new TextArea(getTranslation("jobsummary.dialog.manualcomplete.reason"));
|
||||
reasonField.setWidthFull();
|
||||
reasonField.setMinHeight("100px");
|
||||
reasonField.setRequired(true);
|
||||
|
||||
dialogContent.add(warningText, reasonField);
|
||||
dialog.add(DialogStylingHelper.wrapContent(dialogContent));
|
||||
|
||||
HorizontalLayout buttonBar = new HorizontalLayout();
|
||||
buttonBar.setWidthFull();
|
||||
buttonBar.setJustifyContentMode(HorizontalLayout.JustifyContentMode.END);
|
||||
buttonBar.setSpacing(true);
|
||||
|
||||
Button cancelButton = new Button(getTranslation("jobsummary.dialog.manualcomplete.cancel"),
|
||||
e -> dialog.close());
|
||||
|
||||
Button confirmButton = new Button(getTranslation("jobsummary.dialog.manualcomplete.confirm"));
|
||||
confirmButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_ERROR);
|
||||
confirmButton.addClickListener(e -> {
|
||||
String reason = reasonField.getValue();
|
||||
if (reason == null || reason.trim().isEmpty()) {
|
||||
reasonField.setInvalid(true);
|
||||
reasonField.setErrorMessage(getTranslation("jobsummary.dialog.manualcomplete.reason.required"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
JobStatus oldStatus = job.getStatus();
|
||||
job.setStatus(JobStatus.COMPLETED);
|
||||
job.setUpdatedAt(LocalDateTime.now());
|
||||
jobRepository.save(job);
|
||||
|
||||
String currentUser = securityService.getCurrentUsername();
|
||||
jobHistoryService.logStatusChange(job, oldStatus, JobStatus.COMPLETED, currentUser);
|
||||
|
||||
String description = String.format("Auftrag manuell beendet von %s. Begründung: %s",
|
||||
currentUser, reason.trim());
|
||||
jobHistoryService.logCustomEvent(job.getId(),
|
||||
getTranslation("jobsummary.history.manualcomplete.reason"),
|
||||
description, currentUser, JobHistoryType.STATUS_CHANGE);
|
||||
|
||||
dialog.close();
|
||||
Notification
|
||||
.show(getTranslation("jobsummary.notification.completed", job.getJobNumber()), 3000,
|
||||
Notification.Position.BOTTOM_END)
|
||||
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
|
||||
getUI().ifPresent(ui -> ui.navigate("job_summary/" + job.getId().toHexString()));
|
||||
} catch (Exception ex) {
|
||||
Notification
|
||||
.show(getTranslation("jobsummary.notification.complete.error", ex.getMessage()), 5000,
|
||||
Notification.Position.BOTTOM_END)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
});
|
||||
|
||||
buttonBar.add(cancelButton, confirmButton);
|
||||
dialog.getFooter().add(buttonBar);
|
||||
dialog.open();
|
||||
}
|
||||
|
||||
private VerticalLayout borderedBox() {
|
||||
VerticalLayout box = new VerticalLayout();
|
||||
box.addClassName("summary-card");
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package de.assecutor.votianlt.service;
|
||||
|
||||
import de.assecutor.votianlt.model.AppUser;
|
||||
import de.assecutor.votianlt.model.Job;
|
||||
import de.assecutor.votianlt.model.User;
|
||||
import de.assecutor.votianlt.repository.AppUserRepository;
|
||||
import de.assecutor.votianlt.repository.JobRepository;
|
||||
import de.assecutor.votianlt.repository.UserRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -22,6 +24,7 @@ public class EmailService {
|
||||
private final UserRepository userRepository;
|
||||
private final JobRepository jobRepository;
|
||||
private final TaskAssignmentService taskAssignmentService;
|
||||
private final AppUserRepository appUserRepository;
|
||||
private final JavaMailSender mailSender;
|
||||
|
||||
@Value("${spring.mail.username}")
|
||||
@@ -52,8 +55,10 @@ public class EmailService {
|
||||
return;
|
||||
}
|
||||
|
||||
String completedByName = resolveCompletedByName(job, completedBy);
|
||||
|
||||
// Send email
|
||||
sendEmail(user, job, taskType);
|
||||
sendEmail(user, job, taskType, completedByName);
|
||||
log.info("Task completion notification sent to {} for job {} task {}", user.getEmail(), job.getJobNumber(),
|
||||
taskId);
|
||||
|
||||
@@ -63,7 +68,7 @@ public class EmailService {
|
||||
}
|
||||
}
|
||||
|
||||
private void sendEmail(User user, Job job, String taskType) {
|
||||
private void sendEmail(User user, Job job, String taskType, String completedByName) {
|
||||
SimpleMailMessage message = new SimpleMailMessage();
|
||||
message.setFrom(smtpUsername);
|
||||
message.setTo(user.getEmail());
|
||||
@@ -71,18 +76,17 @@ public class EmailService {
|
||||
"Aufgabe abgeschlossen - " + (job.getJobNumber() != null ? job.getJobNumber() : "Job " + job.getId()));
|
||||
|
||||
String fullName = buildFullName(user);
|
||||
String appUserName = buildAppUserName(user);
|
||||
String taskTypeName = getTaskTypeDisplayName(taskType);
|
||||
|
||||
StringBuilder body = new StringBuilder();
|
||||
body.append("Hallo ").append(fullName).append(",\n\n");
|
||||
body.append("eine Aufgabe wurde von ").append(appUserName).append(" abgeschlossen:\n\n");
|
||||
body.append("eine Aufgabe wurde von ").append(completedByName).append(" abgeschlossen:\n\n");
|
||||
body.append("Job: ").append(job.getJobNumber() != null ? job.getJobNumber() : "Unbekannt").append("\n");
|
||||
if (job.getDeliveryCompany() != null) {
|
||||
body.append("Kunde: ").append(job.getDeliveryCompany()).append("\n");
|
||||
}
|
||||
body.append("Aufgabe: ").append(taskTypeName).append("\n");
|
||||
body.append("Abgeschlossen von: ").append(appUserName).append("\n\n");
|
||||
body.append("Abgeschlossen von: ").append(completedByName).append("\n\n");
|
||||
|
||||
String deliveryCities = job.getDeliveryCitiesDisplay();
|
||||
if (job.getPickupCity() != null || deliveryCities != null) {
|
||||
@@ -121,16 +125,55 @@ public class EmailService {
|
||||
return fullName.isEmpty() ? "Benutzer" : fullName;
|
||||
}
|
||||
|
||||
private String buildAppUserName(User user) {
|
||||
private String buildAppUserName(AppUser appUser) {
|
||||
StringBuilder name = new StringBuilder();
|
||||
if (user.getFirstname() != null && !user.getFirstname().isBlank()) {
|
||||
name.append(user.getFirstname()).append(" ");
|
||||
if (appUser.getVorname() != null && !appUser.getVorname().isBlank()) {
|
||||
name.append(appUser.getVorname()).append(" ");
|
||||
}
|
||||
if (user.getName() != null && !user.getName().isBlank()) {
|
||||
name.append(user.getName());
|
||||
if (appUser.getNachname() != null && !appUser.getNachname().isBlank()) {
|
||||
name.append(appUser.getNachname());
|
||||
}
|
||||
|
||||
String fullName = name.toString().trim();
|
||||
return fullName.isEmpty() ? "App-Benutzer" : fullName;
|
||||
if (!fullName.isEmpty()) {
|
||||
return fullName;
|
||||
}
|
||||
if (appUser.getBezeichnung() != null && !appUser.getBezeichnung().isBlank()) {
|
||||
return appUser.getBezeichnung().trim();
|
||||
}
|
||||
if (appUser.getEmail() != null && !appUser.getEmail().isBlank()) {
|
||||
return appUser.getEmail().trim();
|
||||
}
|
||||
return "App-Benutzer";
|
||||
}
|
||||
|
||||
private String resolveCompletedByName(Job job, String completedBy) {
|
||||
Optional<AppUser> assignedAppUser = findAppUserById(job != null ? job.getAppUser() : null);
|
||||
if (assignedAppUser.isPresent()) {
|
||||
return buildAppUserName(assignedAppUser.get());
|
||||
}
|
||||
|
||||
if (completedBy != null && !completedBy.isBlank() && !"Unknown".equalsIgnoreCase(completedBy)) {
|
||||
Optional<AppUser> completingAppUser = findAppUserById(completedBy);
|
||||
if (completingAppUser.isPresent()) {
|
||||
return buildAppUserName(completingAppUser.get());
|
||||
}
|
||||
return completedBy;
|
||||
}
|
||||
|
||||
return "App-Benutzer";
|
||||
}
|
||||
|
||||
private Optional<AppUser> findAppUserById(String appUserId) {
|
||||
if (appUserId == null || appUserId.isBlank()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
try {
|
||||
return appUserRepository.findById(new ObjectId(appUserId));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private String getTaskTypeDisplayName(String taskType) {
|
||||
@@ -173,8 +216,10 @@ public class EmailService {
|
||||
return;
|
||||
}
|
||||
|
||||
String completedByName = resolveCompletedByName(job, completedBy);
|
||||
|
||||
// Send job completion email
|
||||
sendJobCompletionEmail(user, job);
|
||||
sendJobCompletionEmail(user, job, completedByName);
|
||||
log.info("Job completion notification sent to {} for job {}", user.getEmail(), job.getJobNumber());
|
||||
|
||||
} catch (Exception e) {
|
||||
@@ -182,7 +227,7 @@ public class EmailService {
|
||||
}
|
||||
}
|
||||
|
||||
private void sendJobCompletionEmail(User user, Job job) {
|
||||
private void sendJobCompletionEmail(User user, Job job, String completedByName) {
|
||||
SimpleMailMessage message = new SimpleMailMessage();
|
||||
message.setFrom(smtpUsername);
|
||||
message.setTo(user.getEmail());
|
||||
@@ -190,7 +235,6 @@ public class EmailService {
|
||||
"Job abgeschlossen - " + (job.getJobNumber() != null ? job.getJobNumber() : "Job " + job.getId()));
|
||||
|
||||
String fullName = buildFullName(user);
|
||||
String appUserName = buildAppUserName(user);
|
||||
|
||||
// Count completed tasks
|
||||
var allTasks = taskAssignmentService.findTasksForJob(job);
|
||||
@@ -220,7 +264,7 @@ public class EmailService {
|
||||
}
|
||||
|
||||
body.append("Anzahl erledigter Aufgaben: ").append(taskCount).append("\n");
|
||||
body.append("Abgeschlossen von: ").append(appUserName).append("\n\n");
|
||||
body.append("Abgeschlossen von: ").append(completedByName).append("\n\n");
|
||||
|
||||
body.append("Der Job ist nun vollständig erledigt und kann weiterverarbeitet werden.\n\n");
|
||||
body.append("Mit freundlichen Grüßen,\n");
|
||||
|
||||
@@ -62,7 +62,7 @@ app.security.two-factor.enabled=true
|
||||
|
||||
# WebSocket Configuration
|
||||
app.messaging.websocket.path=/ws/messaging
|
||||
app.messaging.websocket.max-text-message-size=65536
|
||||
app.messaging.websocket.max-text-message-size=10485760
|
||||
app.messaging.websocket.max-session-idle-timeout=300000
|
||||
app.messaging.websocket.allowed-origins=*
|
||||
|
||||
|
||||
@@ -610,6 +610,14 @@ jobsummary.task.photo.taken=Aufgenommene Fotos ({0})
|
||||
jobsummary.task.button.text=Button-Text
|
||||
jobsummary.button.schliessen=Schließen
|
||||
jobsummary.route.planned=Geplante Route
|
||||
jobsummary.button.manualcomplete=Manuell beenden
|
||||
jobsummary.dialog.manualcomplete.title=Auftrag manuell beenden
|
||||
jobsummary.dialog.manualcomplete.text=Der Auftrag {0} wird jetzt manuell abgeschlossen. Danach kann er nicht mehr per App weiter bearbeitet werden.
|
||||
jobsummary.dialog.manualcomplete.reason=Begründung
|
||||
jobsummary.dialog.manualcomplete.reason.required=Bitte geben Sie eine Begründung ein
|
||||
jobsummary.dialog.manualcomplete.cancel=Abbrechen
|
||||
jobsummary.dialog.manualcomplete.confirm=Akzeptiert
|
||||
jobsummary.history.manualcomplete.reason=Manuell beendet
|
||||
|
||||
# Jobs
|
||||
jobs.title=Aufträge
|
||||
|
||||
@@ -557,6 +557,14 @@ jobsummary.task.photo.taken=Tehtud fotod ({0})
|
||||
jobsummary.task.button.text=Nupu tekst
|
||||
jobsummary.button.schliessen=Sulge
|
||||
jobsummary.route.planned=Planeeritud marsruut
|
||||
jobsummary.button.manualcomplete=Lõpeta käsitsi
|
||||
jobsummary.dialog.manualcomplete.title=Lõpeta tellimus käsitsi
|
||||
jobsummary.dialog.manualcomplete.text=Tellimus {0} lõpetatakse nüüd käsitsi. Pärast seda ei saa seda enam rakenduse kaudu töödelda.
|
||||
jobsummary.dialog.manualcomplete.reason=Põhjendus
|
||||
jobsummary.dialog.manualcomplete.reason.required=Palun sisestage põhjendus
|
||||
jobsummary.dialog.manualcomplete.cancel=Tühista
|
||||
jobsummary.dialog.manualcomplete.confirm=Nõustu
|
||||
jobsummary.history.manualcomplete.reason=Käsitsi lõpetatud
|
||||
jobs.title=Tellimused
|
||||
jobs.filter.search=Otsi
|
||||
jobs.filter.search.placeholder=Otsi tellimuse numbri j\u00e4rgi...
|
||||
|
||||
@@ -610,6 +610,14 @@ jobsummary.task.photo.taken=Photos taken ({0})
|
||||
jobsummary.task.button.text=Button Text
|
||||
jobsummary.button.schliessen=Close
|
||||
jobsummary.route.planned=Planned Route
|
||||
jobsummary.button.manualcomplete=Complete manually
|
||||
jobsummary.dialog.manualcomplete.title=Complete job manually
|
||||
jobsummary.dialog.manualcomplete.text=Job {0} will now be completed manually. It can no longer be processed via the app afterwards.
|
||||
jobsummary.dialog.manualcomplete.reason=Reason
|
||||
jobsummary.dialog.manualcomplete.reason.required=Please enter a reason
|
||||
jobsummary.dialog.manualcomplete.cancel=Cancel
|
||||
jobsummary.dialog.manualcomplete.confirm=Accept
|
||||
jobsummary.history.manualcomplete.reason=Manually completed
|
||||
|
||||
# Jobs
|
||||
jobs.title=Jobs
|
||||
|
||||
@@ -610,6 +610,14 @@ jobsummary.task.photo.taken=Fotos tomadas ({0})
|
||||
jobsummary.task.button.text=Texto del bot\u00f3n
|
||||
jobsummary.button.schliessen=Cerrar
|
||||
jobsummary.route.planned=Ruta planificada
|
||||
jobsummary.button.manualcomplete=Finalizar manualmente
|
||||
jobsummary.dialog.manualcomplete.title=Finalizar pedido manualmente
|
||||
jobsummary.dialog.manualcomplete.text=El pedido {0} se completar\u00e1 manualmente. Despu\u00e9s ya no podr\u00e1 ser procesado a trav\u00e9s de la aplicaci\u00f3n.
|
||||
jobsummary.dialog.manualcomplete.reason=Motivo
|
||||
jobsummary.dialog.manualcomplete.reason.required=Por favor, introduzca un motivo
|
||||
jobsummary.dialog.manualcomplete.cancel=Cancelar
|
||||
jobsummary.dialog.manualcomplete.confirm=Aceptar
|
||||
jobsummary.history.manualcomplete.reason=Finalizado manualmente
|
||||
|
||||
# Jobs
|
||||
jobs.title=Pedidos
|
||||
|
||||
@@ -610,6 +610,14 @@ jobsummary.task.photo.taken=Photos prises ({0})
|
||||
jobsummary.task.button.text=Texte du bouton
|
||||
jobsummary.button.schliessen=Fermer
|
||||
jobsummary.route.planned=Itin\u00e9raire pr\u00e9vu
|
||||
jobsummary.button.manualcomplete=Terminer manuellement
|
||||
jobsummary.dialog.manualcomplete.title=Terminer la commande manuellement
|
||||
jobsummary.dialog.manualcomplete.text=La commande {0} va maintenant \u00eatre termin\u00e9e manuellement. Elle ne pourra plus \u00eatre trait\u00e9e via l\u2019application par la suite.
|
||||
jobsummary.dialog.manualcomplete.reason=Motif
|
||||
jobsummary.dialog.manualcomplete.reason.required=Veuillez saisir un motif
|
||||
jobsummary.dialog.manualcomplete.cancel=Annuler
|
||||
jobsummary.dialog.manualcomplete.confirm=Accepter
|
||||
jobsummary.history.manualcomplete.reason=Termin\u00e9 manuellement
|
||||
|
||||
# Jobs
|
||||
jobs.title=Missions
|
||||
|
||||
@@ -610,6 +610,14 @@ jobsummary.task.photo.taken=Padarytos nuotraukos ({0})
|
||||
jobsummary.task.button.text=Mygtuko tekstas
|
||||
jobsummary.button.schliessen=Uždaryti
|
||||
jobsummary.route.planned=Planuotas maršrutas
|
||||
jobsummary.button.manualcomplete=Užbaigti rankiniu būdu
|
||||
jobsummary.dialog.manualcomplete.title=Užbaigti užsakymą rankiniu būdu
|
||||
jobsummary.dialog.manualcomplete.text=Užsakymas {0} dabar bus užbaigtas rankiniu būdu. Po to jo nebebus galima apdoroti per programėlę.
|
||||
jobsummary.dialog.manualcomplete.reason=Priežastis
|
||||
jobsummary.dialog.manualcomplete.reason.required=Prašome įvesti priežastį
|
||||
jobsummary.dialog.manualcomplete.cancel=Atšaukti
|
||||
jobsummary.dialog.manualcomplete.confirm=Priimti
|
||||
jobsummary.history.manualcomplete.reason=Užbaigta rankiniu būdu
|
||||
|
||||
# Jobs
|
||||
jobs.title=Užsakymai
|
||||
|
||||
@@ -610,6 +610,14 @@ jobsummary.task.photo.taken=Uzņemtās fotogrāfijas ({0})
|
||||
jobsummary.task.button.text=Pogas teksts
|
||||
jobsummary.button.schliessen=Aizvērt
|
||||
jobsummary.route.planned=Plānotais maršruts
|
||||
jobsummary.button.manualcomplete=Pabeigt manuāli
|
||||
jobsummary.dialog.manualcomplete.title=Pabeigt pasūtījumu manuāli
|
||||
jobsummary.dialog.manualcomplete.text=Pasūtījums {0} tagad tiks pabeigts manuāli. Pēc tam to vairs nevarēs apstrādāt, izmantojot lietotni.
|
||||
jobsummary.dialog.manualcomplete.reason=Pamatojums
|
||||
jobsummary.dialog.manualcomplete.reason.required=Lūdzu, ievadiet pamatojumu
|
||||
jobsummary.dialog.manualcomplete.cancel=Atcelt
|
||||
jobsummary.dialog.manualcomplete.confirm=Apstiprināt
|
||||
jobsummary.history.manualcomplete.reason=Pabeigts manuāli
|
||||
|
||||
# Jobs
|
||||
jobs.title=Uzdevumi
|
||||
|
||||
@@ -610,6 +610,14 @@ jobsummary.task.photo.taken=Wykonane zdj\u0119cia ({0})
|
||||
jobsummary.task.button.text=Tekst przycisku
|
||||
jobsummary.button.schliessen=Zamknij
|
||||
jobsummary.route.planned=Planowana trasa
|
||||
jobsummary.button.manualcomplete=Zako\u0144cz r\u0119cznie
|
||||
jobsummary.dialog.manualcomplete.title=Zako\u0144cz zlecenie r\u0119cznie
|
||||
jobsummary.dialog.manualcomplete.text=Zlecenie {0} zostanie teraz zako\u0144czone r\u0119cznie. Po tym nie b\u0119dzie mo\u017cna go dalej obs\u0142ugiwa\u0107 przez aplikacj\u0119.
|
||||
jobsummary.dialog.manualcomplete.reason=Uzasadnienie
|
||||
jobsummary.dialog.manualcomplete.reason.required=Prosz\u0119 poda\u0107 uzasadnienie
|
||||
jobsummary.dialog.manualcomplete.cancel=Anuluj
|
||||
jobsummary.dialog.manualcomplete.confirm=Akceptuj
|
||||
jobsummary.history.manualcomplete.reason=Zako\u0144czono r\u0119cznie
|
||||
|
||||
# Jobs
|
||||
jobs.title=Zlecenia
|
||||
|
||||
@@ -610,6 +610,14 @@ jobsummary.task.photo.taken=Сделанные фотографии ({0})
|
||||
jobsummary.task.button.text=Текст кнопки
|
||||
jobsummary.button.schliessen=Закрыть
|
||||
jobsummary.route.planned=Запланированный маршрут
|
||||
jobsummary.button.manualcomplete=Завершить вручную
|
||||
jobsummary.dialog.manualcomplete.title=Завершить заказ вручную
|
||||
jobsummary.dialog.manualcomplete.text=Заказ {0} будет завершён вручную. После этого его больше нельзя будет обрабатывать через приложение.
|
||||
jobsummary.dialog.manualcomplete.reason=Обоснование
|
||||
jobsummary.dialog.manualcomplete.reason.required=Пожалуйста, укажите обоснование
|
||||
jobsummary.dialog.manualcomplete.cancel=Отмена
|
||||
jobsummary.dialog.manualcomplete.confirm=Принять
|
||||
jobsummary.history.manualcomplete.reason=Завершено вручную
|
||||
|
||||
# Jobs
|
||||
jobs.title=Заказы
|
||||
|
||||
@@ -610,6 +610,14 @@ jobsummary.task.photo.taken=\u00c7ekilen Foto\u011fraflar ({0})
|
||||
jobsummary.task.button.text=Buton Metni
|
||||
jobsummary.button.schliessen=Kapat
|
||||
jobsummary.route.planned=Planlanan Rota
|
||||
jobsummary.button.manualcomplete=Manuel olarak tamamla
|
||||
jobsummary.dialog.manualcomplete.title=Siparişi manuel olarak tamamla
|
||||
jobsummary.dialog.manualcomplete.text=Sipariş {0} şimdi manuel olarak tamamlanacak. Bundan sonra uygulama üzerinden işlenemez.
|
||||
jobsummary.dialog.manualcomplete.reason=Gerekçe
|
||||
jobsummary.dialog.manualcomplete.reason.required=Lütfen bir gerekçe girin
|
||||
jobsummary.dialog.manualcomplete.cancel=İptal
|
||||
jobsummary.dialog.manualcomplete.confirm=Kabul et
|
||||
jobsummary.history.manualcomplete.reason=Manuel olarak tamamlandı
|
||||
|
||||
# Jobs
|
||||
jobs.title=\u0130\u015fler
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
package de.assecutor.votianlt.service;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import de.assecutor.votianlt.model.AppUser;
|
||||
import de.assecutor.votianlt.model.Job;
|
||||
import de.assecutor.votianlt.model.User;
|
||||
import de.assecutor.votianlt.model.task.ConfirmationTask;
|
||||
import de.assecutor.votianlt.repository.AppUserRepository;
|
||||
import de.assecutor.votianlt.repository.JobRepository;
|
||||
import de.assecutor.votianlt.repository.TaskRepository;
|
||||
import de.assecutor.votianlt.repository.UserRepository;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.bson.types.ObjectId;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.mail.SimpleMailMessage;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class EmailServiceTest {
|
||||
|
||||
@Mock
|
||||
private UserRepository userRepository;
|
||||
@Mock
|
||||
private JobRepository jobRepository;
|
||||
@Mock
|
||||
private TaskRepository taskRepository;
|
||||
@Mock
|
||||
private AppUserRepository appUserRepository;
|
||||
@Mock
|
||||
private JavaMailSender mailSender;
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<SimpleMailMessage> mailCaptor;
|
||||
|
||||
private EmailService emailService;
|
||||
private TaskAssignmentService taskAssignmentService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
taskAssignmentService = new TaskAssignmentService(taskRepository, jobRepository);
|
||||
emailService = new EmailService(userRepository, jobRepository, taskAssignmentService, appUserRepository,
|
||||
mailSender);
|
||||
ReflectionTestUtils.setField(emailService, "smtpUsername", "noreply@example.com");
|
||||
}
|
||||
|
||||
@Test
|
||||
void sendTaskCompletionNotificationUsesAssignedAppUserName() {
|
||||
User webUser = createWebUser();
|
||||
AppUser assignedAppUser = createAssignedAppUser();
|
||||
Job job = createJob(webUser, assignedAppUser);
|
||||
|
||||
when(jobRepository.findById(job.getId())).thenReturn(Optional.of(job));
|
||||
when(userRepository.findById(webUser.getId())).thenReturn(Optional.of(webUser));
|
||||
when(appUserRepository.findById(assignedAppUser.getId())).thenReturn(Optional.of(assignedAppUser));
|
||||
|
||||
emailService.sendTaskCompletionNotification(job.getId(), "PHOTO", "task-1", "ignored");
|
||||
|
||||
verify(mailSender).send(mailCaptor.capture());
|
||||
String body = mailCaptor.getValue().getText();
|
||||
assertThat(body).contains("eine Aufgabe wurde von Max Mustermann abgeschlossen:");
|
||||
assertThat(body).contains("Abgeschlossen von: Max Mustermann");
|
||||
assertThat(body).contains("Hallo Dr. Anna Unternehmer,");
|
||||
}
|
||||
|
||||
@Test
|
||||
void sendJobCompletionNotificationUsesAssignedAppUserName() {
|
||||
User webUser = createWebUser();
|
||||
AppUser assignedAppUser = createAssignedAppUser();
|
||||
Job job = createJob(webUser, assignedAppUser);
|
||||
|
||||
when(jobRepository.findById(job.getId())).thenReturn(Optional.of(job));
|
||||
when(userRepository.findById(webUser.getId())).thenReturn(Optional.of(webUser));
|
||||
when(appUserRepository.findById(assignedAppUser.getId())).thenReturn(Optional.of(assignedAppUser));
|
||||
when(taskRepository.findByJobIdOrderByTaskOrderAsc(job.getId())).thenReturn(List.of(
|
||||
createConfirmationTask(0, "OK"),
|
||||
createConfirmationTask(1, "Weiter")));
|
||||
|
||||
emailService.sendJobCompletionNotification(job.getId(), "ignored");
|
||||
|
||||
verify(mailSender).send(mailCaptor.capture());
|
||||
String body = mailCaptor.getValue().getText();
|
||||
assertThat(body).contains("Anzahl erledigter Aufgaben: 2");
|
||||
assertThat(body).contains("Abgeschlossen von: Max Mustermann");
|
||||
assertThat(body).contains("Hallo Dr. Anna Unternehmer,");
|
||||
}
|
||||
|
||||
private User createWebUser() {
|
||||
User user = new User();
|
||||
user.setId(new ObjectId());
|
||||
user.setTitle("Dr.");
|
||||
user.setFirstname("Anna");
|
||||
user.setName("Unternehmer");
|
||||
user.setEmail("anna@example.com");
|
||||
return user;
|
||||
}
|
||||
|
||||
private AppUser createAssignedAppUser() {
|
||||
AppUser appUser = new AppUser();
|
||||
appUser.setId(new ObjectId());
|
||||
appUser.setVorname("Max");
|
||||
appUser.setNachname("Mustermann");
|
||||
appUser.setBezeichnung("Fahrer Max");
|
||||
return appUser;
|
||||
}
|
||||
|
||||
private Job createJob(User webUser, AppUser assignedAppUser) {
|
||||
Job job = new Job();
|
||||
job.setId(new ObjectId());
|
||||
job.setCreatedBy(webUser.getId().toHexString());
|
||||
job.setAppUser(assignedAppUser.getId().toHexString());
|
||||
job.setJobNumber("JOB-2026-001");
|
||||
job.setDeliveryCompany("Beispiel GmbH");
|
||||
job.setPickupCity("Berlin");
|
||||
job.setDeliveryCity("Hamburg");
|
||||
return job;
|
||||
}
|
||||
|
||||
private ConfirmationTask createConfirmationTask(int taskOrder, String buttonText) {
|
||||
ConfirmationTask task = new ConfirmationTask(buttonText);
|
||||
task.setTaskOrder(taskOrder);
|
||||
return task;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user