Upload via GUI (55 Dateien)
This commit is contained in:
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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<TicketThreadDto> = emptyList(),
|
||||
val permissions: Map<String, Boolean>? = 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,
|
||||
|
||||
@@ -85,6 +85,9 @@ data class TicketDetail(
|
||||
val replySignatures: ReplySignatures? = null,
|
||||
// Vom Kunden im Formular ausgefüllte Zusatzfelder (Bezeichnung, Wert)
|
||||
val customFields: List<CustomFieldValue> = 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)
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user