From 966c8184413e72f4c9c5d2b2db34eecbfeaf6180 Mon Sep 17 00:00:00 2001 From: M_Viper Date: Thu, 22 Oct 2026 19:00:28 +0000 Subject: [PATCH] Upload via GUI (55 Dateien) --- mobile/build.gradle.kts | 4 +-- .../de/deskhub/staff/data/TicketRepository.kt | 7 +++++ .../deskhub/staff/data/remote/DeskHubApi.kt | 3 +++ .../deskhub/staff/data/remote/dto/ApiDtos.kt | 6 +++++ .../de/deskhub/staff/domain/model/Models.kt | 3 +++ .../java/de/deskhub/staff/ui/DeskHubApp.kt | 27 ++++++++++++++++--- .../de/deskhub/staff/ui/DeskHubViewModel.kt | 9 +++++++ 7 files changed, 54 insertions(+), 5 deletions(-) diff --git a/mobile/build.gradle.kts b/mobile/build.gradle.kts index 180b8338..64d53749 100644 --- a/mobile/build.gradle.kts +++ b/mobile/build.gradle.kts @@ -29,8 +29,8 @@ android { // WICHTIG: Bei jedem neuen Debug-Build erhöhen. Android erkennt eine APK mit // unverändertem versionCode (+ gleicher Signatur) auf manchen Geräten nicht // zuverlässig als Update, wodurch eine Neuinstallation still fehlschlagen kann. - versionCode = 26 - versionName = "2.5.0" + versionCode = 28 + versionName = "2.5.2" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/mobile/src/main/java/de/deskhub/staff/data/TicketRepository.kt b/mobile/src/main/java/de/deskhub/staff/data/TicketRepository.kt index ec3a4abf..e913830d 100644 --- a/mobile/src/main/java/de/deskhub/staff/data/TicketRepository.kt +++ b/mobile/src/main/java/de/deskhub/staff/data/TicketRepository.kt @@ -710,6 +710,11 @@ class TicketRepository( return AvailabilityData(myStatus = r.myStatus, myNote = r.myNote, team = r.team.map(TeamMemberDto::toDomain)) } + /** Hebt die Bearbeitungssperre eines Tickets auf (eigene, als Administrator auch die eines anderen). */ + suspend fun unlockTicket(number: String) { + requireApi().unlockTicket(number) + } + suspend fun setAvailability(status: String, note: String, staffId: Int? = null) { requireApi().setAvailability(status, note, staffId) } // ─── Ticket-Regeln (Automatisierung) ─────────────────────────────────────── @@ -888,6 +893,8 @@ private fun TicketDetailDto.toDomain() = TicketDetail( firstResponseDueAt = firstResponseDueAt, resolutionDueAt = resolutionDueAt, lockedByOtherName = lockedByOtherName, + lockedBy = lockedBy, + canUnlock = canUnlock, threads = threads.map { thread -> TicketThread( id = thread.id, diff --git a/mobile/src/main/java/de/deskhub/staff/data/remote/DeskHubApi.kt b/mobile/src/main/java/de/deskhub/staff/data/remote/DeskHubApi.kt index 0c28ea6d..b030d293 100644 --- a/mobile/src/main/java/de/deskhub/staff/data/remote/DeskHubApi.kt +++ b/mobile/src/main/java/de/deskhub/staff/data/remote/DeskHubApi.kt @@ -497,6 +497,9 @@ class DeskHubApi( suspend fun availability(): AvailabilityResponse = execute("availability.php", "GET", null, AvailabilityResponse::class.java) + suspend fun unlockTicket(number: String): de.deskhub.staff.data.remote.dto.TicketUnlockResponse = + execute("ticket_unlock.php", "POST", de.deskhub.staff.data.remote.dto.TicketUnlockRequest(number), de.deskhub.staff.data.remote.dto.TicketUnlockResponse::class.java) + suspend fun setAvailability(status: String, note: String, staffId: Int? = null): AvailabilitySetResponse = execute("availability.php", "POST", AvailabilitySetRequest(status, note, staffId), AvailabilitySetResponse::class.java) diff --git a/mobile/src/main/java/de/deskhub/staff/data/remote/dto/ApiDtos.kt b/mobile/src/main/java/de/deskhub/staff/data/remote/dto/ApiDtos.kt index d3c07ad6..a7fab590 100644 --- a/mobile/src/main/java/de/deskhub/staff/data/remote/dto/ApiDtos.kt +++ b/mobile/src/main/java/de/deskhub/staff/data/remote/dto/ApiDtos.kt @@ -84,6 +84,9 @@ data class TicketDetailDto( val firstResponseDueAt: String? = null, val resolutionDueAt: String? = null, val lockedByOtherName: String? = null, + // Ein Administrator hat ein von jemand anderem bearbeitetes Ticket geöffnet (Name des anderen) und darf die Sperre aufheben + val lockedBy: String? = null, + val canUnlock: Boolean = false, val threads: List = emptyList(), val permissions: Map? = null, // Signaturen zur Auswahl beim Antworten; null = ältere Serverversion @@ -179,6 +182,9 @@ data class ReplySignaturesDto( data class MessageResponse(val threadId: Int = 0) +data class TicketUnlockRequest(val number: String) +data class TicketUnlockResponse(val ok: Boolean = false, val unlocked: Boolean = false) + /** Gerät für Push-Benachrichtigungen beim Server an- oder abmelden (action: "register" oder "unregister"). */ data class PushDeviceRequest( val action: String, diff --git a/mobile/src/main/java/de/deskhub/staff/domain/model/Models.kt b/mobile/src/main/java/de/deskhub/staff/domain/model/Models.kt index d0e674a4..7184fd48 100644 --- a/mobile/src/main/java/de/deskhub/staff/domain/model/Models.kt +++ b/mobile/src/main/java/de/deskhub/staff/domain/model/Models.kt @@ -85,6 +85,9 @@ data class TicketDetail( val replySignatures: ReplySignatures? = null, // Vom Kunden im Formular ausgefüllte Zusatzfelder (Bezeichnung, Wert) val customFields: List = emptyList(), + // Administrator: Name des Mitarbeiters, der dieses Ticket gerade bearbeitet (Sperre), und ob die Sperre aufgehoben werden darf + val lockedBy: String? = null, + val canUnlock: Boolean = false, ) data class CustomFieldValue(val label: String, val value: String) diff --git a/mobile/src/main/java/de/deskhub/staff/ui/DeskHubApp.kt b/mobile/src/main/java/de/deskhub/staff/ui/DeskHubApp.kt index dcc6f4d5..23807b09 100644 --- a/mobile/src/main/java/de/deskhub/staff/ui/DeskHubApp.kt +++ b/mobile/src/main/java/de/deskhub/staff/ui/DeskHubApp.kt @@ -1,4 +1,4 @@ -@file:OptIn(ExperimentalMaterial3Api::class) +@file:OptIn(ExperimentalMaterial3Api::class, androidx.compose.foundation.layout.ExperimentalLayoutApi::class) package de.deskhub.staff.ui @@ -577,6 +577,7 @@ private fun StaffNavigation(state: DeskHubState, viewModel: DeskHubViewModel) { onUpdate = viewModel::updateTicket, onMessage = viewModel::addMessage, onSpamAction = viewModel::setTicketSpam, + onUnlock = { state.selectedTicket?.summary?.number?.let(viewModel::unlockTicket) }, ) } composable("admin") { @@ -1624,7 +1625,7 @@ private fun DashboardStatusRow(myStatus: String, onSetStatus: (String, String) - fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurfaceVariant, ) - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + androidx.compose.foundation.layout.FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) { AvailabilityStatusLabels.forEach { (value, meta) -> val (label, emoji, color) = meta FilterChip( @@ -2945,7 +2946,7 @@ private fun AdminAvailabilityScreen( Card(modifier = Modifier.fillMaxWidth(), shape = MaterialTheme.shapes.medium) { Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { Text("Mein Status", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold) - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + androidx.compose.foundation.layout.FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) { AvailabilityStatusLabels.forEach { (value, meta) -> val (label, emoji, color) = meta FilterChip( @@ -5638,6 +5639,7 @@ private fun TicketDetailScreen( onUpdate: (String, Int?) -> Unit, onMessage: (String, String, String?, String?, () -> Unit) -> Unit, onSpamAction: (Boolean) -> Unit, + onUnlock: () -> Unit = {}, ) { Scaffold( topBar = { @@ -5671,6 +5673,7 @@ private fun TicketDetailScreen( onUpdate = onUpdate, onMessage = onMessage, onSpamAction = onSpamAction, + onUnlock = onUnlock, ) else -> Box(Modifier.padding(padding)) { EmptyState("Kein Ticket ausgewählt.") } } @@ -5688,6 +5691,7 @@ private fun TicketDetailContent( onUpdate: (String, Int?) -> Unit, onMessage: (String, String, String?, String?, () -> Unit) -> Unit, onSpamAction: (Boolean) -> Unit, + onUnlock: () -> Unit = {}, ) { var selectedStatus by remember(ticket.summary.number) { mutableStateOf(ticket.summary.status) } var selectedAgent by remember(ticket.summary.number) { mutableStateOf(ticket.summary.assignedStaff?.id) } @@ -5721,6 +5725,23 @@ private fun TicketDetailContent( contentPadding = PaddingValues(16.dp), verticalArrangement = Arrangement.spacedBy(14.dp), ) { + // Administrator: Ticket wird gerade von jemand anderem bearbeitet. Lesen und Bearbeiten ist trotzdem möglich, die Sperre lässt sich aufheben. + if (ticket.lockedBy != null) { + item { + Surface(color = Color(0xFFFFFBEB), shape = MaterialTheme.shapes.small) { + Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + "🔒 Dieses Ticket wird gerade von „${ticket.lockedBy}“ bearbeitet. Als Administrator kannst du es trotzdem lesen und bearbeiten.", + style = MaterialTheme.typography.bodySmall, + color = Color(0xFF92400E), + ) + if (ticket.canUnlock) { + Button(onClick = onUnlock) { Text("Sperre aufheben und übernehmen") } + } + } + } + } + } item { Card( modifier = Modifier.fillMaxWidth(), diff --git a/mobile/src/main/java/de/deskhub/staff/ui/DeskHubViewModel.kt b/mobile/src/main/java/de/deskhub/staff/ui/DeskHubViewModel.kt index a0b2c196..110f06cc 100644 --- a/mobile/src/main/java/de/deskhub/staff/ui/DeskHubViewModel.kt +++ b/mobile/src/main/java/de/deskhub/staff/ui/DeskHubViewModel.kt @@ -424,6 +424,15 @@ class DeskHubViewModel( pendingTicket?.let { pendingTicket = null; openTicket(it) } } + /** Administrator: Sperre eines Tickets aufheben und das Ticket danach neu laden (man übernimmt die Sperre). */ + fun unlockTicket(number: String) { + viewModelScope.launch { + runCatching { repository.unlockTicket(number) } + .onSuccess { openTicket(number) } + .onFailure { error -> mutableState.update { it.copy(mutationError = error.userMessage()) } } + } + } + fun openTicket(number: String) { viewModelScope.launch { mutableState.update {