Upload via GUI (55 Dateien)

This commit is contained in:
2026-10-22 19:00:28 +00:00
parent d8ae9da806
commit 966c818441
7 changed files with 54 additions and 5 deletions
@@ -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 {