diff --git a/mobile/build.gradle.kts b/mobile/build.gradle.kts index 64d53749..b5addf48 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 = 28 - versionName = "2.5.2" + versionCode = 30 + versionName = "2.6.1" 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 e913830d..50c35476 100644 --- a/mobile/src/main/java/de/deskhub/staff/data/TicketRepository.kt +++ b/mobile/src/main/java/de/deskhub/staff/data/TicketRepository.kt @@ -427,6 +427,7 @@ class TicketRepository( DepartmentSaveRequest( id = id, name = input.name, email = input.email, isDefault = input.isDefault, signature = input.signature, isActive = input.isActive, managerId = input.managerId, slaPlanId = input.slaPlanId, notes = input.notes, + autoAssignMode = input.autoAssignMode, autoAssignStaffId = input.autoAssignStaffId, ), ).id @@ -794,7 +795,7 @@ private fun CustomerDetailDto.toDomain() = CustomerDetail(id, name, email, organ private fun BanEntryDto.toDomain() = BanEntry(id, matchType, value, reason, addedByName, createdAt) private fun DepartmentDto.toDomain() = Department( id, name, email, isDefault, signature, isActive, managerId, managerName, slaPlanId, notes, - members.map { DepartmentMember(it.id, it.name, it.isPrimary) }, updatedAt, + members.map { DepartmentMember(it.id, it.name, it.isPrimary) }, updatedAt, autoAssignMode, autoAssignStaffId, ) private fun PriorityFullDto.toDomain() = PriorityFull(id, name, color, responseHours, resolutionHours, sortOrder) private fun HelpTopicDto.toDomain() = HelpTopic(id, name, departmentId, departmentName, priorityId, priorityName, priorityColor, isActive, sortOrder, isPublic, parentId, slaPlanId, path.ifBlank { name }, notes, updatedAt) 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 a7fab590..3a2f33d4 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 @@ -463,6 +463,9 @@ data class DepartmentDto( val managerName: String? = null, val slaPlanId: Int? = null, val notes: String? = null, + // Neue Tickets automatisch zuweisen: "none", "staff" (bestimmter Mitarbeiter) oder "round_robin" (reihum) + val autoAssignMode: String = "none", + val autoAssignStaffId: Int? = null, val members: List = emptyList(), val createdAt: String? = null, val updatedAt: String? = null, @@ -488,6 +491,8 @@ data class DepartmentSaveRequest( val managerId: Int? = null, val slaPlanId: Int? = null, val notes: String = "", + val autoAssignMode: String? = null, + val autoAssignStaffId: Int? = null, ) data class DepartmentDeleteRequest(val action: String = "delete", val id: Int) 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 7184fd48..ae118520 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 @@ -313,6 +313,8 @@ data class Department( val notes: String? = null, val members: List = emptyList(), val updatedAt: String? = null, + val autoAssignMode: String = "none", + val autoAssignStaffId: Int? = null, ) data class DepartmentMember(val id: Int, val name: String, val isPrimary: Boolean) @@ -329,6 +331,8 @@ data class DepartmentInput( val slaPlanId: Int?, val notes: String, val signature: String?, + val autoAssignMode: String = "none", + val autoAssignStaffId: Int? = null, ) data class DepartmentData( 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 23807b09..76926137 100644 --- a/mobile/src/main/java/de/deskhub/staff/ui/DeskHubApp.kt +++ b/mobile/src/main/java/de/deskhub/staff/ui/DeskHubApp.kt @@ -1421,6 +1421,13 @@ private fun TicketFilterBar( } } +/** "2026-10-23 14:32:10" (Serverzeit) -> "23.10.2026 14:32"; unbekannte Formate bleiben unverändert. */ +internal fun formatTicketDate(raw: String): String { + val m = Regex("""^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})""").find(raw.trim()) ?: return raw + val (y, mo, d, h, mi) = m.destructured + return "$d.$mo.$y $h:$mi" +} + @Composable private fun TicketCard( ticket: TicketSummary, @@ -1472,6 +1479,12 @@ private fun TicketCard( maxLines = 1, overflow = TextOverflow.Ellipsis, ) + Text( + "Eingang: " + formatTicketDate(ticket.createdAt), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + ) Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp), @@ -3619,6 +3632,11 @@ private fun AdminDepartmentsScreen( var managerExpanded by remember { mutableStateOf(false) } var slaPlanId by rememberSaveable(dept?.id) { mutableStateOf(dept?.slaPlanId) } var slaExpanded by remember { mutableStateOf(false) } + var autoMode by rememberSaveable(dept?.id) { mutableStateOf(dept?.autoAssignMode ?: "none") } + var autoStaffId by rememberSaveable(dept?.id) { mutableStateOf(dept?.autoAssignStaffId) } + var autoModeExpanded by remember { mutableStateOf(false) } + var autoStaffExpanded by remember { mutableStateOf(false) } + val autoModes = listOf("none" to "Nicht automatisch zuweisen", "staff" to "Immer an einen bestimmten Mitarbeiter", "round_robin" to "Reihum an die Mitarbeiter der Abteilung") Column(Modifier.padding(padding).fillMaxSize()) { ScrollableTabRow(selectedTabIndex = tab, edgePadding = 12.dp) { Tab(selected = tab == 0, onClick = { tab = 0 }, text = { Text("Abteilung") }) @@ -3643,6 +3661,15 @@ private fun AdminDepartmentsScreen( DropdownMenuItem(text = { Text("— Keine(r) —") }, onClick = { managerId = null; managerExpanded = false }) state.departmentStaffOptions.forEach { m -> DropdownMenuItem(text = { Text(m.name) }, onClick = { managerId = m.id; managerExpanded = false }) } } + SelectionField(label = "Neue Tickets automatisch zuweisen", value = autoModes.firstOrNull { it.first == autoMode }?.second ?: autoModes[0].second, expanded = autoModeExpanded, onExpand = { autoModeExpanded = true }, onDismiss = { autoModeExpanded = false }) { + autoModes.forEach { (key, label) -> DropdownMenuItem(text = { Text(label) }, onClick = { autoMode = key; autoModeExpanded = false }) } + } + if (autoMode == "staff") { + SelectionField(label = "Mitarbeiter", value = state.departmentStaffOptions.firstOrNull { it.id == autoStaffId }?.name ?: "— Bitte wählen —", expanded = autoStaffExpanded, onExpand = { autoStaffExpanded = true }, onDismiss = { autoStaffExpanded = false }) { + state.departmentStaffOptions.forEach { m -> DropdownMenuItem(text = { Text(m.name) }, onClick = { autoStaffId = m.id; autoStaffExpanded = false }) } + } + } + Text("Gilt für Tickets aus Formular, E-Mail, API und Live-Chat. Eine Ticket-Regel mit „Mitarbeiter zuweisen“ hat Vorrang. Im Urlaub bekommt die Vertretung das Ticket, „Reihum“ überspringt Mitarbeiter im Urlaub oder offline.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) SelectionField(label = "SLA-Plan", value = state.departmentSlaPlans.firstOrNull { it.id == slaPlanId }?.name ?: "Zeiten der Priorität verwenden", expanded = slaExpanded, onExpand = { slaExpanded = true }, onDismiss = { slaExpanded = false }) { DropdownMenuItem(text = { Text("Zeiten der Priorität verwenden") }, onClick = { slaPlanId = null; slaExpanded = false }) state.departmentSlaPlans.forEach { p -> DropdownMenuItem(text = { Text(p.name) }, onClick = { slaPlanId = p.id; slaExpanded = false }) } @@ -3692,7 +3719,7 @@ private fun AdminDepartmentsScreen( Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { Button( onClick = { - onSave(dept?.id, DepartmentInput(name.trim(), email.trim(), isDefault, isActive, managerId, slaPlanId, notes.trim(), signature.trim().ifBlank { null })) { editingId = null; isCreating = false; tab = 0 } + onSave(dept?.id, DepartmentInput(name.trim(), email.trim(), isDefault, isActive, managerId, slaPlanId, notes.trim(), signature.trim().ifBlank { null }, autoMode, if (autoMode == "staff") autoStaffId else null)) { editingId = null; isCreating = false; tab = 0 } }, shape = MaterialTheme.shapes.medium, enabled = !state.departments2MutationLoading && name.isNotBlank(), diff --git a/mobile/src/test/java/de/deskhub/staff/ui/TicketDateTest.kt b/mobile/src/test/java/de/deskhub/staff/ui/TicketDateTest.kt new file mode 100644 index 00000000..6d8ea162 --- /dev/null +++ b/mobile/src/test/java/de/deskhub/staff/ui/TicketDateTest.kt @@ -0,0 +1,21 @@ +package de.deskhub.staff.ui + +import org.junit.Assert.assertEquals +import org.junit.Test + +/** Eingangsdatum in der Ticketliste: "2026-10-23 14:32:10" wird zu "23.10.2026 14:32". */ +class TicketDateTest { + @Test + fun formatsServerTimestamp() { + assertEquals("23.10.2026 14:32", formatTicketDate("2026-10-23 14:32:10")) + assertEquals("01.01.2027 00:05", formatTicketDate("2027-01-01 00:05:00")) + assertEquals("23.10.2026 14:32", formatTicketDate("2026-10-23T14:32:10")) + assertEquals("23.10.2026 14:32", formatTicketDate(" 2026-10-23 14:32 ")) + } + + @Test + fun keepsUnknownFormats() { + assertEquals("gestern", formatTicketDate("gestern")) + assertEquals("", formatTicketDate("")) + } +}