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",
|
"id": "5:2194624907249454848",
|
||||||
"lastPropertyId": "6:5035828038544573244",
|
"lastPropertyId": "7:5673785903451668117",
|
||||||
"name": "TaskStatusEntity",
|
"name": "TaskStatusEntity",
|
||||||
"properties": [
|
"properties": [
|
||||||
{
|
{
|
||||||
@@ -275,7 +275,9 @@
|
|||||||
"modelVersionParserMinimum": 5,
|
"modelVersionParserMinimum": 5,
|
||||||
"retiredEntityUids": [],
|
"retiredEntityUids": [],
|
||||||
"retiredIndexUids": [],
|
"retiredIndexUids": [],
|
||||||
"retiredPropertyUids": [],
|
"retiredPropertyUids": [
|
||||||
|
5673785903451668117
|
||||||
|
],
|
||||||
"retiredRelationUids": [],
|
"retiredRelationUids": [],
|
||||||
"version": 1
|
"version": 1
|
||||||
}
|
}
|
||||||
@@ -240,7 +240,7 @@ final _entities = <obx_int.ModelEntity>[
|
|||||||
obx_int.ModelEntity(
|
obx_int.ModelEntity(
|
||||||
id: const obx_int.IdUid(5, 2194624907249454848),
|
id: const obx_int.IdUid(5, 2194624907249454848),
|
||||||
name: 'TaskStatusEntity',
|
name: 'TaskStatusEntity',
|
||||||
lastPropertyId: const obx_int.IdUid(6, 5035828038544573244),
|
lastPropertyId: const obx_int.IdUid(7, 5673785903451668117),
|
||||||
flags: 0,
|
flags: 0,
|
||||||
properties: <obx_int.ModelProperty>[
|
properties: <obx_int.ModelProperty>[
|
||||||
obx_int.ModelProperty(
|
obx_int.ModelProperty(
|
||||||
@@ -371,7 +371,7 @@ obx_int.ModelDefinition getObjectBoxModel() {
|
|||||||
lastSequenceId: const obx_int.IdUid(0, 0),
|
lastSequenceId: const obx_int.IdUid(0, 0),
|
||||||
retiredEntityUids: const [],
|
retiredEntityUids: const [],
|
||||||
retiredIndexUids: const [],
|
retiredIndexUids: const [],
|
||||||
retiredPropertyUids: const [],
|
retiredPropertyUids: const [5673785903451668117],
|
||||||
retiredRelationUids: const [],
|
retiredRelationUids: const [],
|
||||||
modelVersion: 5,
|
modelVersion: 5,
|
||||||
modelVersionParserMinimum: 5,
|
modelVersionParserMinimum: 5,
|
||||||
@@ -632,7 +632,7 @@ obx_int.ModelDefinition getObjectBoxModel() {
|
|||||||
},
|
},
|
||||||
objectToFB: (TaskStatusEntity object, fb.Builder fbb) {
|
objectToFB: (TaskStatusEntity object, fb.Builder fbb) {
|
||||||
final taskIdOffset = fbb.writeString(object.taskId);
|
final taskIdOffset = fbb.writeString(object.taskId);
|
||||||
fbb.startTable(7);
|
fbb.startTable(8);
|
||||||
fbb.addInt64(0, object.id);
|
fbb.addInt64(0, object.id);
|
||||||
fbb.addOffset(1, taskIdOffset);
|
fbb.addOffset(1, taskIdOffset);
|
||||||
fbb.addBool(2, object.completed);
|
fbb.addBool(2, object.completed);
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ class TaskView extends StatefulWidget {
|
|||||||
|
|
||||||
class _TaskViewState extends State<TaskView> {
|
class _TaskViewState extends State<TaskView> {
|
||||||
final Set<String> _completedTasks = {};
|
final Set<String> _completedTasks = {};
|
||||||
final Set<String> _skippedTasks = {};
|
|
||||||
final DatabaseService _databaseService = DatabaseService();
|
final DatabaseService _databaseService = DatabaseService();
|
||||||
// Store SVG representations of signatures per task for later use
|
// Store SVG representations of signatures per task for later use
|
||||||
final Map<String, String> _signatureSvgByTask = {};
|
final Map<String, String> _signatureSvgByTask = {};
|
||||||
@@ -61,7 +60,7 @@ class _TaskViewState extends State<TaskView> {
|
|||||||
.toList();
|
.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 {
|
Future<void> _loadTaskStatuses() async {
|
||||||
final statuses = await _databaseService.loadAllTaskStatuses();
|
final statuses = await _databaseService.loadAllTaskStatuses();
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -170,33 +169,26 @@ class _TaskViewState extends State<TaskView> {
|
|||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final task = _visibleTasks[index];
|
final task = _visibleTasks[index];
|
||||||
final isCompleted = _completedTasks.contains(task.id);
|
final isCompleted = _completedTasks.contains(task.id);
|
||||||
final isSkipped = _skippedTasks.contains(task.id);
|
|
||||||
final canBeCompletedNow =
|
final canBeCompletedNow =
|
||||||
!isCompleted && !isSkipped && _arePreviousTasksCompleted(index);
|
!isCompleted && _arePreviousTasksCompleted(index);
|
||||||
|
|
||||||
// Hintergrundfarbe je nach Status:
|
// Hintergrundfarbe je nach Status:
|
||||||
// abgeschlossen → hellgrün, übersprungen → hellgelb, bearbeitbar → weiß, gesperrt → hellgrau
|
// abgeschlossen → hellgrün, bearbeitbar → weiß, gesperrt → hellgrau
|
||||||
final Color cardColor =
|
final Color cardColor =
|
||||||
isCompleted
|
isCompleted
|
||||||
? const Color(0xFFE8F5E9) // hellgrün
|
? const Color(0xFFE8F5E9) // hellgrün
|
||||||
: isSkipped
|
|
||||||
? const Color(0xFFFFF8E1) // hellgelb
|
|
||||||
: canBeCompletedNow
|
: canBeCompletedNow
|
||||||
? Colors.white
|
? Colors.white
|
||||||
: const Color(0xFFF5F5F5); // hellgrau
|
: const Color(0xFFF5F5F5); // hellgrau
|
||||||
final Color borderColor =
|
final Color borderColor =
|
||||||
isCompleted
|
isCompleted
|
||||||
? Colors.green[300]!
|
? Colors.green[300]!
|
||||||
: isSkipped
|
|
||||||
? Colors.amber[300]!
|
|
||||||
: canBeCompletedNow
|
: canBeCompletedNow
|
||||||
? Colors.grey[300]!
|
? Colors.grey[300]!
|
||||||
: Colors.grey[200]!;
|
: Colors.grey[200]!;
|
||||||
final Color circleColor =
|
final Color circleColor =
|
||||||
isCompleted
|
isCompleted
|
||||||
? Colors.green[600]!
|
? Colors.green[600]!
|
||||||
: isSkipped
|
|
||||||
? Colors.amber[600]!
|
|
||||||
: canBeCompletedNow
|
: canBeCompletedNow
|
||||||
? Colors.deepPurple[400]!
|
? Colors.deepPurple[400]!
|
||||||
: Colors.grey[400]!;
|
: Colors.grey[400]!;
|
||||||
@@ -250,7 +242,7 @@ class _TaskViewState extends State<TaskView> {
|
|||||||
children: [
|
children: [
|
||||||
_buildTaskDisplayText(
|
_buildTaskDisplayText(
|
||||||
task,
|
task,
|
||||||
isCompleted || isSkipped,
|
isCompleted,
|
||||||
index,
|
index,
|
||||||
),
|
),
|
||||||
if (_getTaskStationLabel(task) != null) ...[
|
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),
|
const SizedBox(width: 8),
|
||||||
Icon(Icons.check_circle, color: Colors.green[600]),
|
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;
|
if (index <= 0) return true;
|
||||||
for (int i = 0; i < index; i++) {
|
for (int i = 0; i < index; i++) {
|
||||||
final t = _visibleTasks[i];
|
final t = _visibleTasks[i];
|
||||||
if (!t.optional &&
|
if (!t.optional && !_completedTasks.contains(t.id)) {
|
||||||
!_completedTasks.contains(t.id) &&
|
|
||||||
!_skippedTasks.contains(t.id)) {
|
|
||||||
return false;
|
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
|
# 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
|
# 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.
|
# 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:
|
environment:
|
||||||
sdk: ^3.7.0
|
sdk: ^3.7.0
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ set -euo pipefail
|
|||||||
|
|
||||||
readonly SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
readonly SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
readonly REGISTRY_IMAGE="registry.assecutor.org/votianlt"
|
readonly REGISTRY_IMAGE="registry.assecutor.org/votianlt"
|
||||||
readonly BACKEND_DIR="${SCRIPT_DIR}/backend"
|
readonly BACKEND_DIR="${SCRIPT_DIR}"
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<'EOF'
|
cat <<'EOF'
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<revision>0.9.15</revision>
|
<revision>0.9.16</revision>
|
||||||
<java.version>21</java.version>
|
<java.version>21</java.version>
|
||||||
<maven.compiler.source>21</maven.compiler.source>
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
<maven.compiler.target>21</maven.compiler.target>
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package de.assecutor.votianlt.messaging;
|
package de.assecutor.votianlt.messaging;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.web.socket.config.annotation.EnableWebSocket;
|
import org.springframework.web.socket.config.annotation.EnableWebSocket;
|
||||||
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
|
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
|
||||||
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
|
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
|
||||||
|
import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;
|
||||||
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
|
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,6 +25,12 @@ public class WebSocketConfig implements WebSocketConfigurer {
|
|||||||
@Value("${app.messaging.websocket.allowed-origins:*}")
|
@Value("${app.messaging.websocket.allowed-origins:*}")
|
||||||
private String allowedOrigins;
|
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) {
|
public WebSocketConfig(WebSocketService webSocketService) {
|
||||||
this.webSocketService = webSocketService;
|
this.webSocketService = webSocketService;
|
||||||
}
|
}
|
||||||
@@ -32,4 +40,13 @@ public class WebSocketConfig implements WebSocketConfigurer {
|
|||||||
registry.addHandler(webSocketService, wsPath).setAllowedOrigins(allowedOrigins.split(","))
|
registry.addHandler(webSocketService, wsPath).setAllowedOrigins(allowedOrigins.split(","))
|
||||||
.addInterceptors(new HttpSessionHandshakeInterceptor());
|
.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;
|
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() {
|
private void handleApplyStations() {
|
||||||
|
removeEmptyDeliveryStations();
|
||||||
revealPriceAndDetailsSection();
|
revealPriceAndDetailsSection();
|
||||||
|
|
||||||
if (!areAllStationsValidatedByGoogle()) {
|
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.notification.NotificationVariant;
|
||||||
import com.vaadin.flow.component.icon.Icon;
|
import com.vaadin.flow.component.icon.Icon;
|
||||||
import com.vaadin.flow.component.icon.VaadinIcon;
|
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.AttachEvent;
|
||||||
import com.vaadin.flow.component.DetachEvent;
|
import com.vaadin.flow.component.DetachEvent;
|
||||||
import com.vaadin.flow.component.UI;
|
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.LocationService;
|
||||||
import de.assecutor.votianlt.service.MessageService;
|
import de.assecutor.votianlt.service.MessageService;
|
||||||
import de.assecutor.votianlt.service.TaskAssignmentService;
|
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 de.assecutor.votianlt.util.DateTimeFormatUtil;
|
||||||
import jakarta.annotation.security.RolesAllowed;
|
import jakarta.annotation.security.RolesAllowed;
|
||||||
import org.bson.types.ObjectId;
|
import org.bson.types.ObjectId;
|
||||||
@@ -88,6 +91,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
|||||||
private final LocationService locationService;
|
private final LocationService locationService;
|
||||||
private final ServiceRepository serviceRepository;
|
private final ServiceRepository serviceRepository;
|
||||||
private final TaskAssignmentService taskAssignmentService;
|
private final TaskAssignmentService taskAssignmentService;
|
||||||
|
private final SecurityService securityService;
|
||||||
|
|
||||||
@Value("${app.google.maps.api-key}")
|
@Value("${app.google.maps.api-key}")
|
||||||
private String googleMapsApiKey;
|
private String googleMapsApiKey;
|
||||||
@@ -103,7 +107,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
|||||||
PhotoRepository photoRepository, CommentRepository commentRepository, AppUserService appUserService,
|
PhotoRepository photoRepository, CommentRepository commentRepository, AppUserService appUserService,
|
||||||
MessageService messageService, JobHistoryService jobHistoryService,
|
MessageService messageService, JobHistoryService jobHistoryService,
|
||||||
JobUpdateBroadcaster jobUpdateBroadcaster, LocationService locationService,
|
JobUpdateBroadcaster jobUpdateBroadcaster, LocationService locationService,
|
||||||
ServiceRepository serviceRepository, TaskAssignmentService taskAssignmentService) {
|
ServiceRepository serviceRepository, TaskAssignmentService taskAssignmentService,
|
||||||
|
SecurityService securityService) {
|
||||||
this.jobRepository = jobRepository;
|
this.jobRepository = jobRepository;
|
||||||
this.cargoItemRepository = cargoItemRepository;
|
this.cargoItemRepository = cargoItemRepository;
|
||||||
this.signatureRepository = signatureRepository;
|
this.signatureRepository = signatureRepository;
|
||||||
@@ -116,6 +121,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
|||||||
this.locationService = locationService;
|
this.locationService = locationService;
|
||||||
this.serviceRepository = serviceRepository;
|
this.serviceRepository = serviceRepository;
|
||||||
this.taskAssignmentService = taskAssignmentService;
|
this.taskAssignmentService = taskAssignmentService;
|
||||||
|
this.securityService = securityService;
|
||||||
|
|
||||||
setSizeFull();
|
setSizeFull();
|
||||||
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN,
|
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));
|
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
|
// Create Job History Button for toolbar
|
||||||
Button jobHistoryButton = new Button(getTranslation("jobsummary.button.jobhistory"));
|
Button jobHistoryButton = new Button(getTranslation("jobsummary.button.jobhistory"));
|
||||||
jobHistoryButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
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()));
|
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 toolbar with buttons
|
||||||
|
if (manualCompleteButton != null) {
|
||||||
|
add(new ViewToolbar(getTranslation("jobsummary.title"), manualCompleteButton, sendMessageButton,
|
||||||
|
jobHistoryButton));
|
||||||
|
} else {
|
||||||
add(new ViewToolbar(getTranslation("jobsummary.title"), sendMessageButton, jobHistoryButton));
|
add(new ViewToolbar(getTranslation("jobsummary.title"), sendMessageButton, jobHistoryButton));
|
||||||
|
}
|
||||||
|
|
||||||
List<CargoItem> cargo = cargoItemRepository.findByJobId(currentJobId);
|
List<CargoItem> cargo = cargoItemRepository.findByJobId(currentJobId);
|
||||||
List<BaseTask> tasks = taskAssignmentService.findTasksForJob(job);
|
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() {
|
private VerticalLayout borderedBox() {
|
||||||
VerticalLayout box = new VerticalLayout();
|
VerticalLayout box = new VerticalLayout();
|
||||||
box.addClassName("summary-card");
|
box.addClassName("summary-card");
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package de.assecutor.votianlt.service;
|
package de.assecutor.votianlt.service;
|
||||||
|
|
||||||
|
import de.assecutor.votianlt.model.AppUser;
|
||||||
import de.assecutor.votianlt.model.Job;
|
import de.assecutor.votianlt.model.Job;
|
||||||
import de.assecutor.votianlt.model.User;
|
import de.assecutor.votianlt.model.User;
|
||||||
|
import de.assecutor.votianlt.repository.AppUserRepository;
|
||||||
import de.assecutor.votianlt.repository.JobRepository;
|
import de.assecutor.votianlt.repository.JobRepository;
|
||||||
import de.assecutor.votianlt.repository.UserRepository;
|
import de.assecutor.votianlt.repository.UserRepository;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -22,6 +24,7 @@ public class EmailService {
|
|||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
private final JobRepository jobRepository;
|
private final JobRepository jobRepository;
|
||||||
private final TaskAssignmentService taskAssignmentService;
|
private final TaskAssignmentService taskAssignmentService;
|
||||||
|
private final AppUserRepository appUserRepository;
|
||||||
private final JavaMailSender mailSender;
|
private final JavaMailSender mailSender;
|
||||||
|
|
||||||
@Value("${spring.mail.username}")
|
@Value("${spring.mail.username}")
|
||||||
@@ -52,8 +55,10 @@ public class EmailService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String completedByName = resolveCompletedByName(job, completedBy);
|
||||||
|
|
||||||
// Send email
|
// 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(),
|
log.info("Task completion notification sent to {} for job {} task {}", user.getEmail(), job.getJobNumber(),
|
||||||
taskId);
|
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();
|
SimpleMailMessage message = new SimpleMailMessage();
|
||||||
message.setFrom(smtpUsername);
|
message.setFrom(smtpUsername);
|
||||||
message.setTo(user.getEmail());
|
message.setTo(user.getEmail());
|
||||||
@@ -71,18 +76,17 @@ public class EmailService {
|
|||||||
"Aufgabe abgeschlossen - " + (job.getJobNumber() != null ? job.getJobNumber() : "Job " + job.getId()));
|
"Aufgabe abgeschlossen - " + (job.getJobNumber() != null ? job.getJobNumber() : "Job " + job.getId()));
|
||||||
|
|
||||||
String fullName = buildFullName(user);
|
String fullName = buildFullName(user);
|
||||||
String appUserName = buildAppUserName(user);
|
|
||||||
String taskTypeName = getTaskTypeDisplayName(taskType);
|
String taskTypeName = getTaskTypeDisplayName(taskType);
|
||||||
|
|
||||||
StringBuilder body = new StringBuilder();
|
StringBuilder body = new StringBuilder();
|
||||||
body.append("Hallo ").append(fullName).append(",\n\n");
|
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");
|
body.append("Job: ").append(job.getJobNumber() != null ? job.getJobNumber() : "Unbekannt").append("\n");
|
||||||
if (job.getDeliveryCompany() != null) {
|
if (job.getDeliveryCompany() != null) {
|
||||||
body.append("Kunde: ").append(job.getDeliveryCompany()).append("\n");
|
body.append("Kunde: ").append(job.getDeliveryCompany()).append("\n");
|
||||||
}
|
}
|
||||||
body.append("Aufgabe: ").append(taskTypeName).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();
|
String deliveryCities = job.getDeliveryCitiesDisplay();
|
||||||
if (job.getPickupCity() != null || deliveryCities != null) {
|
if (job.getPickupCity() != null || deliveryCities != null) {
|
||||||
@@ -121,16 +125,55 @@ public class EmailService {
|
|||||||
return fullName.isEmpty() ? "Benutzer" : fullName;
|
return fullName.isEmpty() ? "Benutzer" : fullName;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildAppUserName(User user) {
|
private String buildAppUserName(AppUser appUser) {
|
||||||
StringBuilder name = new StringBuilder();
|
StringBuilder name = new StringBuilder();
|
||||||
if (user.getFirstname() != null && !user.getFirstname().isBlank()) {
|
if (appUser.getVorname() != null && !appUser.getVorname().isBlank()) {
|
||||||
name.append(user.getFirstname()).append(" ");
|
name.append(appUser.getVorname()).append(" ");
|
||||||
}
|
}
|
||||||
if (user.getName() != null && !user.getName().isBlank()) {
|
if (appUser.getNachname() != null && !appUser.getNachname().isBlank()) {
|
||||||
name.append(user.getName());
|
name.append(appUser.getNachname());
|
||||||
}
|
}
|
||||||
|
|
||||||
String fullName = name.toString().trim();
|
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) {
|
private String getTaskTypeDisplayName(String taskType) {
|
||||||
@@ -173,8 +216,10 @@ public class EmailService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String completedByName = resolveCompletedByName(job, completedBy);
|
||||||
|
|
||||||
// Send job completion email
|
// Send job completion email
|
||||||
sendJobCompletionEmail(user, job);
|
sendJobCompletionEmail(user, job, completedByName);
|
||||||
log.info("Job completion notification sent to {} for job {}", user.getEmail(), job.getJobNumber());
|
log.info("Job completion notification sent to {} for job {}", user.getEmail(), job.getJobNumber());
|
||||||
|
|
||||||
} catch (Exception e) {
|
} 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();
|
SimpleMailMessage message = new SimpleMailMessage();
|
||||||
message.setFrom(smtpUsername);
|
message.setFrom(smtpUsername);
|
||||||
message.setTo(user.getEmail());
|
message.setTo(user.getEmail());
|
||||||
@@ -190,7 +235,6 @@ public class EmailService {
|
|||||||
"Job abgeschlossen - " + (job.getJobNumber() != null ? job.getJobNumber() : "Job " + job.getId()));
|
"Job abgeschlossen - " + (job.getJobNumber() != null ? job.getJobNumber() : "Job " + job.getId()));
|
||||||
|
|
||||||
String fullName = buildFullName(user);
|
String fullName = buildFullName(user);
|
||||||
String appUserName = buildAppUserName(user);
|
|
||||||
|
|
||||||
// Count completed tasks
|
// Count completed tasks
|
||||||
var allTasks = taskAssignmentService.findTasksForJob(job);
|
var allTasks = taskAssignmentService.findTasksForJob(job);
|
||||||
@@ -220,7 +264,7 @@ public class EmailService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body.append("Anzahl erledigter Aufgaben: ").append(taskCount).append("\n");
|
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("Der Job ist nun vollständig erledigt und kann weiterverarbeitet werden.\n\n");
|
||||||
body.append("Mit freundlichen Grüßen,\n");
|
body.append("Mit freundlichen Grüßen,\n");
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ app.security.two-factor.enabled=true
|
|||||||
|
|
||||||
# WebSocket Configuration
|
# WebSocket Configuration
|
||||||
app.messaging.websocket.path=/ws/messaging
|
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.max-session-idle-timeout=300000
|
||||||
app.messaging.websocket.allowed-origins=*
|
app.messaging.websocket.allowed-origins=*
|
||||||
|
|
||||||
|
|||||||
@@ -610,6 +610,14 @@ jobsummary.task.photo.taken=Aufgenommene Fotos ({0})
|
|||||||
jobsummary.task.button.text=Button-Text
|
jobsummary.task.button.text=Button-Text
|
||||||
jobsummary.button.schliessen=Schließen
|
jobsummary.button.schliessen=Schließen
|
||||||
jobsummary.route.planned=Geplante Route
|
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
|
||||||
jobs.title=Aufträge
|
jobs.title=Aufträge
|
||||||
|
|||||||
@@ -557,6 +557,14 @@ jobsummary.task.photo.taken=Tehtud fotod ({0})
|
|||||||
jobsummary.task.button.text=Nupu tekst
|
jobsummary.task.button.text=Nupu tekst
|
||||||
jobsummary.button.schliessen=Sulge
|
jobsummary.button.schliessen=Sulge
|
||||||
jobsummary.route.planned=Planeeritud marsruut
|
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.title=Tellimused
|
||||||
jobs.filter.search=Otsi
|
jobs.filter.search=Otsi
|
||||||
jobs.filter.search.placeholder=Otsi tellimuse numbri j\u00e4rgi...
|
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.task.button.text=Button Text
|
||||||
jobsummary.button.schliessen=Close
|
jobsummary.button.schliessen=Close
|
||||||
jobsummary.route.planned=Planned Route
|
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
|
||||||
jobs.title=Jobs
|
jobs.title=Jobs
|
||||||
|
|||||||
@@ -610,6 +610,14 @@ jobsummary.task.photo.taken=Fotos tomadas ({0})
|
|||||||
jobsummary.task.button.text=Texto del bot\u00f3n
|
jobsummary.task.button.text=Texto del bot\u00f3n
|
||||||
jobsummary.button.schliessen=Cerrar
|
jobsummary.button.schliessen=Cerrar
|
||||||
jobsummary.route.planned=Ruta planificada
|
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
|
||||||
jobs.title=Pedidos
|
jobs.title=Pedidos
|
||||||
|
|||||||
@@ -610,6 +610,14 @@ jobsummary.task.photo.taken=Photos prises ({0})
|
|||||||
jobsummary.task.button.text=Texte du bouton
|
jobsummary.task.button.text=Texte du bouton
|
||||||
jobsummary.button.schliessen=Fermer
|
jobsummary.button.schliessen=Fermer
|
||||||
jobsummary.route.planned=Itin\u00e9raire pr\u00e9vu
|
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
|
||||||
jobs.title=Missions
|
jobs.title=Missions
|
||||||
|
|||||||
@@ -610,6 +610,14 @@ jobsummary.task.photo.taken=Padarytos nuotraukos ({0})
|
|||||||
jobsummary.task.button.text=Mygtuko tekstas
|
jobsummary.task.button.text=Mygtuko tekstas
|
||||||
jobsummary.button.schliessen=Uždaryti
|
jobsummary.button.schliessen=Uždaryti
|
||||||
jobsummary.route.planned=Planuotas maršrutas
|
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
|
||||||
jobs.title=Užsakymai
|
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.task.button.text=Pogas teksts
|
||||||
jobsummary.button.schliessen=Aizvērt
|
jobsummary.button.schliessen=Aizvērt
|
||||||
jobsummary.route.planned=Plānotais maršruts
|
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
|
||||||
jobs.title=Uzdevumi
|
jobs.title=Uzdevumi
|
||||||
|
|||||||
@@ -610,6 +610,14 @@ jobsummary.task.photo.taken=Wykonane zdj\u0119cia ({0})
|
|||||||
jobsummary.task.button.text=Tekst przycisku
|
jobsummary.task.button.text=Tekst przycisku
|
||||||
jobsummary.button.schliessen=Zamknij
|
jobsummary.button.schliessen=Zamknij
|
||||||
jobsummary.route.planned=Planowana trasa
|
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
|
||||||
jobs.title=Zlecenia
|
jobs.title=Zlecenia
|
||||||
|
|||||||
@@ -610,6 +610,14 @@ jobsummary.task.photo.taken=Сделанные фотографии ({0})
|
|||||||
jobsummary.task.button.text=Текст кнопки
|
jobsummary.task.button.text=Текст кнопки
|
||||||
jobsummary.button.schliessen=Закрыть
|
jobsummary.button.schliessen=Закрыть
|
||||||
jobsummary.route.planned=Запланированный маршрут
|
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
|
||||||
jobs.title=Заказы
|
jobs.title=Заказы
|
||||||
|
|||||||
@@ -610,6 +610,14 @@ jobsummary.task.photo.taken=\u00c7ekilen Foto\u011fraflar ({0})
|
|||||||
jobsummary.task.button.text=Buton Metni
|
jobsummary.task.button.text=Buton Metni
|
||||||
jobsummary.button.schliessen=Kapat
|
jobsummary.button.schliessen=Kapat
|
||||||
jobsummary.route.planned=Planlanan Rota
|
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
|
||||||
jobs.title=\u0130\u015fler
|
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