Upload via GUI (2911 Dateien)
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -64,6 +64,7 @@ import de.deskhub.staff.domain.model.ActivityUpdate
|
|||||||
import de.deskhub.staff.domain.model.AuditLogEntry
|
import de.deskhub.staff.domain.model.AuditLogEntry
|
||||||
import de.deskhub.staff.domain.model.AuditLogPage
|
import de.deskhub.staff.domain.model.AuditLogPage
|
||||||
import de.deskhub.staff.domain.model.BanEntry
|
import de.deskhub.staff.domain.model.BanEntry
|
||||||
|
import de.deskhub.staff.domain.model.PermissionGroup
|
||||||
import de.deskhub.staff.domain.model.BusinessHoliday
|
import de.deskhub.staff.domain.model.BusinessHoliday
|
||||||
import de.deskhub.staff.domain.model.BusinessHour
|
import de.deskhub.staff.domain.model.BusinessHour
|
||||||
import de.deskhub.staff.domain.model.BusinessHoursData
|
import de.deskhub.staff.domain.model.BusinessHoursData
|
||||||
@@ -257,6 +258,20 @@ class TicketRepository(
|
|||||||
suspend fun setFollowing(number: String, following: Boolean): TicketSummary =
|
suspend fun setFollowing(number: String, following: Boolean): TicketSummary =
|
||||||
requireApi().updateTicket(linkedMapOf("number" to number, "following" to following)).ticket.toDomain()
|
requireApi().updateTicket(linkedMapOf("number" to number, "following" to following)).ticket.toDomain()
|
||||||
|
|
||||||
|
/** E-Mail-Abruf von Hand: Ergebnis (ohne Probleme?, fertiger Text). */
|
||||||
|
suspend fun fetchMailNow(): Pair<Boolean, String> {
|
||||||
|
val r = requireApi().mailFetch()
|
||||||
|
return Pair(r.ok || r.level != "error", r.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Anhänge, Bilder und Formatierung der Original-E-Mail nachladen: fertiger Text. */
|
||||||
|
suspend fun reloadMailAttachments(number: String): String {
|
||||||
|
val r = requireApi().ticketMailReload(number)
|
||||||
|
val n = r.added + r.inlineImages
|
||||||
|
val base = if (n > 0 || r.formatting) "Anhänge und Bilder aus der E-Mail nachgeladen." else "Es gab nichts Neues zu laden."
|
||||||
|
return if (r.failed.isEmpty()) base else base + " " + r.failed.joinToString(" | ")
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun setTicketSpam(number: String, restore: Boolean): SpamActionResult {
|
suspend fun setTicketSpam(number: String, restore: Boolean): SpamActionResult {
|
||||||
val response = requireApi().ticketSpam(number, if (restore) "restore" else "mark")
|
val response = requireApi().ticketSpam(number, if (restore) "restore" else "mark")
|
||||||
return SpamActionResult(
|
return SpamActionResult(
|
||||||
@@ -588,6 +603,8 @@ class TicketRepository(
|
|||||||
permissionKeys = r.permissionKeys,
|
permissionKeys = r.permissionKeys,
|
||||||
defaultAgentPermissions = r.defaultAgentPermissions,
|
defaultAgentPermissions = r.defaultAgentPermissions,
|
||||||
roles = r.items.map(RoleDto::toDomain),
|
roles = r.items.map(RoleDto::toDomain),
|
||||||
|
permissionLabels = r.permissionLabels,
|
||||||
|
permissionGroups = r.permissionGroups.map { PermissionGroup(it.title, it.keys) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -832,7 +849,7 @@ private fun PriorityFullDto.toDomain() = PriorityFull(id, name, color, responseH
|
|||||||
private fun HelpTopicDto.toDomain() = HelpTopic(id, name, departmentId, departmentName, priorityId, priorityName, priorityColor, isActive, sortOrder, isPublic, parentId, slaPlanId, path.ifBlank { name }, notes, updatedAt)
|
private fun HelpTopicDto.toDomain() = HelpTopic(id, name, departmentId, departmentName, priorityId, priorityName, priorityColor, isActive, sortOrder, isPublic, parentId, slaPlanId, path.ifBlank { name }, notes, updatedAt)
|
||||||
private fun OrganizationDto.toDomain() = Organization(id, name, notes, memberCount)
|
private fun OrganizationDto.toDomain() = Organization(id, name, notes, memberCount)
|
||||||
private fun TeamDto.toDomain() = Team(id, name, leadId, leadName, notes, isActive, memberCount, memberIds)
|
private fun TeamDto.toDomain() = Team(id, name, leadId, leadName, notes, isActive, memberCount, memberIds)
|
||||||
private fun RoleDto.toDomain() = Role(id, name, permissions, staffCount)
|
private fun RoleDto.toDomain() = Role(id, name, permissions, staffCount, effectivePermissions)
|
||||||
private fun StaffAdminDto.toDomain() = StaffAdmin(
|
private fun StaffAdminDto.toDomain() = StaffAdmin(
|
||||||
id, name, email, role, roleId, departmentId, departmentName, isActive, signature,
|
id, name, email, role, roleId, departmentId, departmentName, isActive, signature,
|
||||||
extraDepartments.map { StaffExtraDept(it.departmentId, it.notify, it.roleId) },
|
extraDepartments.map { StaffExtraDept(it.departmentId, it.notify, it.roleId) },
|
||||||
|
|||||||
@@ -99,6 +99,9 @@ import de.deskhub.staff.data.remote.dto.UploadImageResponse
|
|||||||
import de.deskhub.staff.data.remote.dto.TicketResponse
|
import de.deskhub.staff.data.remote.dto.TicketResponse
|
||||||
import de.deskhub.staff.data.remote.dto.TicketSpamRequest
|
import de.deskhub.staff.data.remote.dto.TicketSpamRequest
|
||||||
import de.deskhub.staff.data.remote.dto.TicketSpamResponse
|
import de.deskhub.staff.data.remote.dto.TicketSpamResponse
|
||||||
|
import de.deskhub.staff.data.remote.dto.MailFetchResponse
|
||||||
|
import de.deskhub.staff.data.remote.dto.MailReloadRequest
|
||||||
|
import de.deskhub.staff.data.remote.dto.MailReloadResponse
|
||||||
import de.deskhub.staff.data.remote.dto.TicketSummaryResponse
|
import de.deskhub.staff.data.remote.dto.TicketSummaryResponse
|
||||||
import de.deskhub.staff.data.remote.dto.TimeEntriesResponse
|
import de.deskhub.staff.data.remote.dto.TimeEntriesResponse
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -196,6 +199,12 @@ class DeskHubApi(
|
|||||||
suspend fun addMessage(request: MessageRequest): MessageResponse =
|
suspend fun addMessage(request: MessageRequest): MessageResponse =
|
||||||
execute("ticket_message.php", "POST", request, MessageResponse::class.java)
|
execute("ticket_message.php", "POST", request, MessageResponse::class.java)
|
||||||
|
|
||||||
|
suspend fun mailFetch(): MailFetchResponse =
|
||||||
|
execute("mail_fetch.php", "POST", emptyMap<String, String>(), MailFetchResponse::class.java)
|
||||||
|
|
||||||
|
suspend fun ticketMailReload(number: String): MailReloadResponse =
|
||||||
|
execute("ticket_mail_reload.php", "POST", MailReloadRequest(number = number), MailReloadResponse::class.java)
|
||||||
|
|
||||||
suspend fun ticketSpam(number: String, action: String): TicketSpamResponse =
|
suspend fun ticketSpam(number: String, action: String): TicketSpamResponse =
|
||||||
execute("ticket_spam.php", "POST", TicketSpamRequest(number = number, action = action), TicketSpamResponse::class.java)
|
execute("ticket_spam.php", "POST", TicketSpamRequest(number = number, action = action), TicketSpamResponse::class.java)
|
||||||
|
|
||||||
|
|||||||
@@ -200,6 +200,31 @@ data class CustomFieldValueDto(
|
|||||||
val value: String = "",
|
val value: String = "",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** E-Mail-Abruf von Hand (Antwort der Schnittstelle mail_fetch.php). */
|
||||||
|
data class MailFetchResponse(
|
||||||
|
val ok: Boolean = false,
|
||||||
|
val level: String = "info",
|
||||||
|
val message: String = "",
|
||||||
|
val processed: Int = 0,
|
||||||
|
val newTickets: Int = 0,
|
||||||
|
val replies: Int = 0,
|
||||||
|
val available: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Anhänge, eingebettete Bilder und Formatierung der Original-E-Mail nachladen (ticket_mail_reload.php). */
|
||||||
|
data class MailReloadRequest(
|
||||||
|
val number: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MailReloadResponse(
|
||||||
|
val added: Int = 0,
|
||||||
|
val inlineImages: Int = 0,
|
||||||
|
val formatting: Boolean = false,
|
||||||
|
val skipped: Int = 0,
|
||||||
|
val alreadyThere: Int = 0,
|
||||||
|
val failed: List<String> = emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
data class TicketSpamRequest(
|
data class TicketSpamRequest(
|
||||||
val number: String,
|
val number: String,
|
||||||
val action: String,
|
val action: String,
|
||||||
@@ -817,11 +842,20 @@ data class RoleDto(
|
|||||||
val id: Int = 0,
|
val id: Int = 0,
|
||||||
val name: String = "",
|
val name: String = "",
|
||||||
val permissions: Map<String, Boolean> = emptyMap(),
|
val permissions: Map<String, Boolean> = emptyMap(),
|
||||||
|
/** Wirksame Rechte (fehlende Rechte der Rolle mit Standardwert aufgefüllt); ältere Server liefern das nicht. */
|
||||||
|
val effectivePermissions: Map<String, Boolean>? = null,
|
||||||
val staffCount: Int = 0,
|
val staffCount: Int = 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class PermissionGroupDto(
|
||||||
|
val title: String = "",
|
||||||
|
val keys: List<String> = emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
data class RoleListResponse(
|
data class RoleListResponse(
|
||||||
val permissionKeys: List<String> = emptyList(),
|
val permissionKeys: List<String> = emptyList(),
|
||||||
|
val permissionLabels: Map<String, String> = emptyMap(),
|
||||||
|
val permissionGroups: List<PermissionGroupDto> = emptyList(),
|
||||||
val defaultAgentPermissions: Map<String, Boolean> = emptyMap(),
|
val defaultAgentPermissions: Map<String, Boolean> = emptyMap(),
|
||||||
val items: List<RoleDto> = emptyList(),
|
val items: List<RoleDto> = emptyList(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -456,12 +456,17 @@ data class Role(
|
|||||||
val name: String,
|
val name: String,
|
||||||
val permissions: Map<String, Boolean>,
|
val permissions: Map<String, Boolean>,
|
||||||
val staffCount: Int,
|
val staffCount: Int,
|
||||||
|
val effectivePermissions: Map<String, Boolean>? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class PermissionGroup(val title: String, val keys: List<String>)
|
||||||
|
|
||||||
data class RoleData(
|
data class RoleData(
|
||||||
val permissionKeys: List<String>,
|
val permissionKeys: List<String>,
|
||||||
val defaultAgentPermissions: Map<String, Boolean>,
|
val defaultAgentPermissions: Map<String, Boolean>,
|
||||||
val roles: List<Role>,
|
val roles: List<Role>,
|
||||||
|
val permissionLabels: Map<String, String> = emptyMap(),
|
||||||
|
val permissionGroups: List<PermissionGroup> = emptyList(),
|
||||||
)
|
)
|
||||||
|
|
||||||
// ─── Admin: Mitarbeiter ──────────────────────────────────────────────────────
|
// ─── Admin: Mitarbeiter ──────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -289,6 +289,7 @@ private fun StaffNavigation(state: DeskHubState, viewModel: DeskHubViewModel) {
|
|||||||
navController.navigate("detail")
|
navController.navigate("detail")
|
||||||
},
|
},
|
||||||
onLogout = viewModel::logout,
|
onLogout = viewModel::logout,
|
||||||
|
onFetchMail = viewModel::fetchMailNow,
|
||||||
onNewTicket = {
|
onNewTicket = {
|
||||||
viewModel.startNewTicket()
|
viewModel.startNewTicket()
|
||||||
navController.navigate("newTicket")
|
navController.navigate("newTicket")
|
||||||
@@ -394,6 +395,7 @@ private fun StaffNavigation(state: DeskHubState, viewModel: DeskHubViewModel) {
|
|||||||
onUpdate = viewModel::updateTicket,
|
onUpdate = viewModel::updateTicket,
|
||||||
onMessage = viewModel::addMessage,
|
onMessage = viewModel::addMessage,
|
||||||
onSpamAction = viewModel::setTicketSpam,
|
onSpamAction = viewModel::setTicketSpam,
|
||||||
|
onReloadMailAttachments = viewModel::reloadMailAttachments,
|
||||||
onSnooze = viewModel::snoozeTicket,
|
onSnooze = viewModel::snoozeTicket,
|
||||||
onFollow = viewModel::setFollowing,
|
onFollow = viewModel::setFollowing,
|
||||||
onAddCc = viewModel::addCollaborator,
|
onAddCc = viewModel::addCollaborator,
|
||||||
@@ -452,7 +454,7 @@ private fun StaffNavigation(state: DeskHubState, viewModel: DeskHubViewModel) {
|
|||||||
onBack = { navController.popBackStack() },
|
onBack = { navController.popBackStack() },
|
||||||
onLoad = viewModel::loadAvailability,
|
onLoad = viewModel::loadAvailability,
|
||||||
onSetStatus = viewModel::setMyAvailability,
|
onSetStatus = viewModel::setMyAvailability,
|
||||||
isAdmin = state.staff?.role == "admin",
|
isAdmin = state.can("manage_availability"),
|
||||||
onSetMemberStatus = viewModel::setMemberAvailability,
|
onSetMemberStatus = viewModel::setMemberAvailability,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -908,6 +910,7 @@ private fun TicketListScreen(
|
|||||||
onLoadViews: () -> Unit,
|
onLoadViews: () -> Unit,
|
||||||
onTicket: (String) -> Unit,
|
onTicket: (String) -> Unit,
|
||||||
onLogout: () -> Unit,
|
onLogout: () -> Unit,
|
||||||
|
onFetchMail: () -> Unit,
|
||||||
onNewTicket: () -> Unit,
|
onNewTicket: () -> Unit,
|
||||||
) {
|
) {
|
||||||
LaunchedEffect(Unit) { onLoadViews() }
|
LaunchedEffect(Unit) { onLoadViews() }
|
||||||
@@ -938,6 +941,7 @@ private fun TicketListScreen(
|
|||||||
Text("Tickets", style = MaterialTheme.typography.headlineMedium)
|
Text("Tickets", style = MaterialTheme.typography.headlineMedium)
|
||||||
Text("${state.ticketsTotal} insgesamt", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
Text("${state.ticketsTotal} insgesamt", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
}
|
}
|
||||||
|
if (state.can("fetch_mail")) androidx.compose.material3.TextButton(onClick = onFetchMail, enabled = !state.mutationLoading) { Text("📥 E-Mails abrufen") }
|
||||||
IconButton(onClick = onRefresh) { Icon(Icons.Filled.Refresh, contentDescription = "Aktualisieren") }
|
IconButton(onClick = onRefresh) { Icon(Icons.Filled.Refresh, contentDescription = "Aktualisieren") }
|
||||||
}
|
}
|
||||||
TicketFilterBar(
|
TicketFilterBar(
|
||||||
@@ -951,6 +955,7 @@ private fun TicketListScreen(
|
|||||||
onDeleteView = onDeleteView,
|
onDeleteView = onDeleteView,
|
||||||
)
|
)
|
||||||
state.mutationError?.let { ErrorText(it, Modifier.padding(horizontal = 16.dp)) }
|
state.mutationError?.let { ErrorText(it, Modifier.padding(horizontal = 16.dp)) }
|
||||||
|
state.mutationInfo?.let { Text(it, modifier = Modifier.padding(horizontal = 20.dp, vertical = 4.dp), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) }
|
||||||
when {
|
when {
|
||||||
state.ticketsLoading && state.tickets.isEmpty() -> LoadingState("Tickets werden geladen …")
|
state.ticketsLoading && state.tickets.isEmpty() -> LoadingState("Tickets werden geladen …")
|
||||||
state.ticketsError != null && state.tickets.isEmpty() -> ErrorState(state.ticketsError, onRefresh)
|
state.ticketsError != null && state.tickets.isEmpty() -> ErrorState(state.ticketsError, onRefresh)
|
||||||
@@ -1059,7 +1064,7 @@ private fun TicketFilterBar(
|
|||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
Text("Speichert die aktuellen Filter.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
Text("Speichert die aktuellen Filter.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
OutlinedTextField(value = viewName, onValueChange = { viewName = it }, label = { Text("Name") }, singleLine = true, shape = MaterialTheme.shapes.medium)
|
OutlinedTextField(value = viewName, onValueChange = { viewName = it }, label = { Text("Name") }, singleLine = true, shape = MaterialTheme.shapes.medium)
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
if (metadata?.staff?.permissions?.get("share_views") == true) Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Checkbox(checked = viewShared, onCheckedChange = { viewShared = it })
|
Checkbox(checked = viewShared, onCheckedChange = { viewShared = it })
|
||||||
Text("Für alle Mitarbeiter sichtbar")
|
Text("Für alle Mitarbeiter sichtbar")
|
||||||
}
|
}
|
||||||
@@ -1286,7 +1291,7 @@ private fun TicketFilterBar(
|
|||||||
FilterChip(
|
FilterChip(
|
||||||
selected = filters.snoozed,
|
selected = filters.snoozed,
|
||||||
onClick = { onFiltersChanged(filters.copy(snoozed = !filters.snoozed)); onSearch() },
|
onClick = { onFiltersChanged(filters.copy(snoozed = !filters.snoozed)); onSearch() },
|
||||||
label = { Text("💤 Wiedervorlage") },
|
label = { Text("Wiedervorlage") },
|
||||||
shape = CircleShape,
|
shape = CircleShape,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -4110,13 +4115,41 @@ private val AdminSectionPermissionLabels = linkedMapOf(
|
|||||||
"manage_business_hours" to "Geschäftszeiten verwalten",
|
"manage_business_hours" to "Geschäftszeiten verwalten",
|
||||||
)
|
)
|
||||||
|
|
||||||
private val RolePermissionLabels = TicketPermissionLabels + AdminSectionPermissionLabels
|
/** Rechte, die mit Server 1.0.85 dazugekommen sind (Beschriftungen für ältere Server ohne „permissionLabels“). */
|
||||||
|
private val NewPermissionLabels = linkedMapOf(
|
||||||
|
"merge_split_ticket" to "Tickets zusammenführen und teilen",
|
||||||
|
"link_tickets" to "Tickets verknüpfen",
|
||||||
|
"view_customers" to "Kundenprofile ansehen",
|
||||||
|
"manage_customers" to "Kundenverzeichnis verwalten",
|
||||||
|
"manage_banlist" to "Sperrliste und Absender sperren",
|
||||||
|
"schedule_reports" to "Berichte per E-Mail versenden",
|
||||||
|
"export_tickets" to "Tickets als CSV exportieren",
|
||||||
|
"fetch_mail" to "E-Mails jetzt abrufen",
|
||||||
|
"view_mail_queue" to "Mail-Warteschlange verwalten",
|
||||||
|
"manage_ticket_templates" to "Ticket-Vorlagen verwalten",
|
||||||
|
"manage_site_pages" to "Seiten im Kundenportal verwalten",
|
||||||
|
"manage_languages" to "Sprachen verwalten",
|
||||||
|
"manage_status_page" to "Statusseite verwalten",
|
||||||
|
"share_views" to "Ansichten mit dem Team teilen",
|
||||||
|
"manage_custom_fields" to "Ticket-Felder und Listen verwalten",
|
||||||
|
"manage_automation" to "Ticket-Regeln und wiederkehrende Tickets verwalten",
|
||||||
|
"manage_problems" to "Problemmanagement",
|
||||||
|
"manage_availability" to "Status und Schichten der Kollegen ändern",
|
||||||
|
"view_login_guard" to "Anmelde-Wächter ansehen",
|
||||||
|
"view_help" to "Hilfe und Handbuch"
|
||||||
|
)
|
||||||
|
|
||||||
|
private val RolePermissionLabels = TicketPermissionLabels + AdminSectionPermissionLabels + NewPermissionLabels
|
||||||
|
|
||||||
|
/** Hat der angemeldete Mitarbeiter dieses Recht? Echte Administratoren haben alle. */
|
||||||
|
private fun DeskHubState.can(permission: String): Boolean = staff?.role == "admin" || staff?.permissions?.get(permission) == true
|
||||||
|
|
||||||
/** Ist true für echte Admins UND für Agenten, denen eine der obigen Berechtigungen zugewiesen wurde. */
|
/** Ist true für echte Admins UND für Agenten, denen eine der obigen Berechtigungen zugewiesen wurde. */
|
||||||
private fun Staff?.hasAnyAdminAccess(): Boolean {
|
private fun Staff?.hasAnyAdminAccess(): Boolean {
|
||||||
if (this == null) return false
|
if (this == null) return false
|
||||||
if (role == "admin") return true
|
if (role == "admin") return true
|
||||||
return AdminSectionPermissionLabels.keys.any { permissions[it] == true } || permissions["view_reports"] == true
|
return AdminSectionPermissionLabels.keys.any { permissions[it] == true } || permissions["view_reports"] == true ||
|
||||||
|
permissions["manage_automation"] == true || permissions["manage_banlist"] == true || permissions["manage_availability"] == true
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -4149,32 +4182,27 @@ private fun AdminRolesScreen(
|
|||||||
showForm -> {
|
showForm -> {
|
||||||
val role = data.roles.firstOrNull { it.id == editingId }
|
val role = data.roles.firstOrNull { it.id == editingId }
|
||||||
var name by rememberSaveable(role?.id) { mutableStateOf(role?.name ?: "") }
|
var name by rememberSaveable(role?.id) { mutableStateOf(role?.name ?: "") }
|
||||||
var perms by rememberSaveable(role?.id) { mutableStateOf(role?.permissions ?: emptyMap()) }
|
var perms by rememberSaveable(role?.id) { mutableStateOf(role?.effectivePermissions ?: role?.permissions ?: data.defaultAgentPermissions) }
|
||||||
|
val permLabels = RolePermissionLabels + data.permissionLabels
|
||||||
|
val permGroups: List<Pair<String, List<String>>> = if (data.permissionGroups.isNotEmpty()) data.permissionGroups.map { it.title to it.keys }
|
||||||
|
else listOf("Ticket-Berechtigungen" to TicketPermissionLabels.keys.toList(), "Adminportal-Bereiche (\"Co-Admin\")" to AdminSectionPermissionLabels.keys.toList())
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(padding).fillMaxSize().verticalScroll(rememberScrollState()).padding(16.dp),
|
modifier = Modifier.padding(padding).fillMaxSize().verticalScroll(rememberScrollState()).padding(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
Text(if (role == null) "Neue Rolle" else "Rolle bearbeiten", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
|
Text(if (role == null) "Neue Rolle" else "Rolle bearbeiten", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
|
||||||
OutlinedTextField(value = name, onValueChange = { name = it }, modifier = Modifier.fillMaxWidth(), label = { Text("Name") }, singleLine = true, shape = MaterialTheme.shapes.medium)
|
OutlinedTextField(value = name, onValueChange = { name = it }, modifier = Modifier.fillMaxWidth(), label = { Text("Name") }, singleLine = true, shape = MaterialTheme.shapes.medium)
|
||||||
Text("Ticket-Berechtigungen", style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.SemiBold)
|
permGroups.forEach { (groupTitle, keys) ->
|
||||||
TicketPermissionLabels.forEach { (key, label) ->
|
Text(groupTitle, style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.SemiBold)
|
||||||
|
keys.forEach { key ->
|
||||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||||
Checkbox(
|
Checkbox(
|
||||||
checked = perms[key] == true,
|
checked = perms[key] == true,
|
||||||
onCheckedChange = { checked -> perms = perms + (key to checked) },
|
onCheckedChange = { checked -> perms = perms + (key to checked) },
|
||||||
)
|
)
|
||||||
Text(label, style = MaterialTheme.typography.bodyMedium)
|
Text(permLabels[key] ?: key, style = MaterialTheme.typography.bodyMedium)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Text("Adminportal-Bereiche (\"Co-Admin\")", style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.SemiBold)
|
|
||||||
AdminSectionPermissionLabels.forEach { (key, label) ->
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
|
||||||
Checkbox(
|
|
||||||
checked = perms[key] == true,
|
|
||||||
onCheckedChange = { checked -> perms = perms + (key to checked) },
|
|
||||||
)
|
|
||||||
Text(label, style = MaterialTheme.typography.bodyMedium)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
state.rolesMutationError?.let { ErrorText(it) }
|
state.rolesMutationError?.let { ErrorText(it) }
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
@@ -4201,7 +4229,7 @@ private fun RoleRowAdmin(role: Role, onEdit: () -> Unit, onDelete: () -> Unit) {
|
|||||||
Column(Modifier.padding(14.dp)) {
|
Column(Modifier.padding(14.dp)) {
|
||||||
Text(role.name, fontWeight = FontWeight.SemiBold)
|
Text(role.name, fontWeight = FontWeight.SemiBold)
|
||||||
Text("${role.staffCount} Mitarbeiter", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
Text("${role.staffCount} Mitarbeiter", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
val granted = role.permissions.filterValues { it }.keys.mapNotNull { RolePermissionLabels[it] }
|
val granted = (role.effectivePermissions ?: role.permissions).filterValues { it }.keys.mapNotNull { RolePermissionLabels[it] }
|
||||||
Text(
|
Text(
|
||||||
if (granted.isEmpty()) "Keine Berechtigungen" else granted.joinToString(", "),
|
if (granted.isEmpty()) "Keine Berechtigungen" else granted.joinToString(", "),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
@@ -5368,6 +5396,7 @@ private fun TicketDetailScreen(
|
|||||||
onUpdate: (String, Int?, String?) -> Unit,
|
onUpdate: (String, Int?, String?) -> Unit,
|
||||||
onMessage: (String, String, String?, String?, String?, () -> Unit) -> Unit,
|
onMessage: (String, String, String?, String?, String?, () -> Unit) -> Unit,
|
||||||
onSpamAction: (Boolean) -> Unit,
|
onSpamAction: (Boolean) -> Unit,
|
||||||
|
onReloadMailAttachments: () -> Unit,
|
||||||
onSnooze: (String?) -> Unit = {},
|
onSnooze: (String?) -> Unit = {},
|
||||||
onFollow: (Boolean) -> Unit = {},
|
onFollow: (Boolean) -> Unit = {},
|
||||||
onAddCc: (String) -> Unit = {},
|
onAddCc: (String) -> Unit = {},
|
||||||
@@ -5402,6 +5431,8 @@ private fun TicketDetailScreen(
|
|||||||
onUpdate = onUpdate,
|
onUpdate = onUpdate,
|
||||||
onMessage = onMessage,
|
onMessage = onMessage,
|
||||||
onSpamAction = onSpamAction,
|
onSpamAction = onSpamAction,
|
||||||
|
onReloadMailAttachments = onReloadMailAttachments,
|
||||||
|
mutationInfo = state.mutationInfo,
|
||||||
onSnooze = onSnooze,
|
onSnooze = onSnooze,
|
||||||
onFollow = onFollow,
|
onFollow = onFollow,
|
||||||
onAddCc = onAddCc,
|
onAddCc = onAddCc,
|
||||||
@@ -5421,9 +5452,11 @@ private fun TicketDetailContent(
|
|||||||
cannedResponses: List<de.deskhub.staff.domain.model.CannedResponse>,
|
cannedResponses: List<de.deskhub.staff.domain.model.CannedResponse>,
|
||||||
mutationLoading: Boolean,
|
mutationLoading: Boolean,
|
||||||
mutationError: String?,
|
mutationError: String?,
|
||||||
|
mutationInfo: String?,
|
||||||
onUpdate: (String, Int?, String?) -> Unit,
|
onUpdate: (String, Int?, String?) -> Unit,
|
||||||
onMessage: (String, String, String?, String?, String?, () -> Unit) -> Unit,
|
onMessage: (String, String, String?, String?, String?, () -> Unit) -> Unit,
|
||||||
onSpamAction: (Boolean) -> Unit,
|
onSpamAction: (Boolean) -> Unit,
|
||||||
|
onReloadMailAttachments: () -> Unit = {},
|
||||||
onSnooze: (String?) -> Unit = {},
|
onSnooze: (String?) -> Unit = {},
|
||||||
onFollow: (Boolean) -> Unit = {},
|
onFollow: (Boolean) -> Unit = {},
|
||||||
onAddCc: (String) -> Unit = {},
|
onAddCc: (String) -> Unit = {},
|
||||||
@@ -5574,6 +5607,14 @@ private fun TicketDetailContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ticket.source == "email") {
|
if (ticket.source == "email") {
|
||||||
|
item {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = onReloadMailAttachments,
|
||||||
|
enabled = !mutationLoading,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) { Text("📎 Anhänge und Bilder aus E-Mail laden") }
|
||||||
|
mutationInfo?.let { Text(it, modifier = Modifier.padding(top = 6.dp), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) }
|
||||||
|
}
|
||||||
item {
|
item {
|
||||||
if (!ticket.summary.isSpam) {
|
if (!ticket.summary.isSpam) {
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ data class DeskHubState(
|
|||||||
val detailError: String? = null,
|
val detailError: String? = null,
|
||||||
val mutationLoading: Boolean = false,
|
val mutationLoading: Boolean = false,
|
||||||
val mutationError: String? = null,
|
val mutationError: String? = null,
|
||||||
|
val mutationInfo: String? = null,
|
||||||
val dashboard: DashboardStats? = null,
|
val dashboard: DashboardStats? = null,
|
||||||
val dashboardLoading: Boolean = false,
|
val dashboardLoading: Boolean = false,
|
||||||
val dashboardError: String? = null,
|
val dashboardError: String? = null,
|
||||||
@@ -696,6 +697,43 @@ class DeskHubViewModel(
|
|||||||
|
|
||||||
fun statusClearMessage() = updateStatusPage { it.copy(message = null, error = null) }
|
fun statusClearMessage() = updateStatusPage { it.copy(message = null, error = null) }
|
||||||
|
|
||||||
|
/** E-Mail-Abruf von Hand starten (Knopf in der Ticketliste). */
|
||||||
|
fun fetchMailNow() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
mutableState.update { it.copy(mutationLoading = true, mutationError = null, mutationInfo = null) }
|
||||||
|
runCatching { repository.fetchMailNow() }
|
||||||
|
.onSuccess { (ok, message) ->
|
||||||
|
mutableState.update {
|
||||||
|
if (ok) it.copy(mutationLoading = false, mutationInfo = message) else it.copy(mutationLoading = false, mutationError = message)
|
||||||
|
}
|
||||||
|
loadTickets(reset = true)
|
||||||
|
kotlinx.coroutines.delay(8000)
|
||||||
|
mutableState.update { it.copy(mutationInfo = null) }
|
||||||
|
}
|
||||||
|
.onFailure { error ->
|
||||||
|
mutableState.update { it.copy(mutationLoading = false, mutationError = error.userMessage()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Lädt Anhänge, eingebettete Bilder und die Formatierung der Original-E-Mail des geöffneten Tickets nach. */
|
||||||
|
fun reloadMailAttachments() {
|
||||||
|
val ticket = state.value.selectedTicket ?: return
|
||||||
|
viewModelScope.launch {
|
||||||
|
mutableState.update { it.copy(mutationLoading = true, mutationError = null, mutationInfo = null) }
|
||||||
|
runCatching { repository.reloadMailAttachments(ticket.summary.number) }
|
||||||
|
.onSuccess { message ->
|
||||||
|
mutableState.update { it.copy(mutationLoading = false, mutationInfo = message) }
|
||||||
|
openTicket(ticket.summary.number)
|
||||||
|
kotlinx.coroutines.delay(8000)
|
||||||
|
mutableState.update { it.copy(mutationInfo = null) }
|
||||||
|
}
|
||||||
|
.onFailure { error ->
|
||||||
|
mutableState.update { it.copy(mutationLoading = false, mutationError = error.userMessage()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Markiert das aktuell geöffnete Ticket als Spam, oder macht das (restore = true) rückgängig. */
|
/** Markiert das aktuell geöffnete Ticket als Spam, oder macht das (restore = true) rückgängig. */
|
||||||
fun setTicketSpam(restore: Boolean) {
|
fun setTicketSpam(restore: Boolean) {
|
||||||
val ticket = state.value.selectedTicket ?: return
|
val ticket = state.value.selectedTicket ?: return
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ fun MoreScreen(staff: Staff?, onNavigate: (String) -> Unit, onLogout: () -> Unit
|
|||||||
|
|
||||||
val personal = listOf(
|
val personal = listOf(
|
||||||
MoreEntry(Icons.AutoMirrored.Filled.Assignment, "Aufgaben", "To-dos für dich und dein Team", "adminTasks"),
|
MoreEntry(Icons.AutoMirrored.Filled.Assignment, "Aufgaben", "To-dos für dich und dein Team", "adminTasks"),
|
||||||
MoreEntry(Icons.Filled.Circle, if (isFullAdmin) "Status der Mitarbeiter" else "Mein Status", "Verfügbarkeit und Team-Übersicht", "adminAvailability"),
|
MoreEntry(Icons.Filled.Circle, if (allowed("manage_availability")) "Status der Mitarbeiter" else "Mein Status", "Verfügbarkeit und Team-Übersicht", "adminAvailability"),
|
||||||
MoreEntry(Icons.Filled.Notifications, "Benachrichtigungen", "Popups und stille Zeiten", "pushPrefs"),
|
MoreEntry(Icons.Filled.Notifications, "Benachrichtigungen", "Popups und stille Zeiten", "pushPrefs"),
|
||||||
)
|
)
|
||||||
val manage = buildList {
|
val manage = buildList {
|
||||||
@@ -135,8 +135,8 @@ fun MoreScreen(staff: Staff?, onNavigate: (String) -> Unit, onLogout: () -> Unit
|
|||||||
if (allowed("view_time_entries")) add(MoreEntry(Icons.Filled.AccessTime, "Zeiterfassung", "Zeiten je Mitarbeiter", "adminTimeEntries"))
|
if (allowed("view_time_entries")) add(MoreEntry(Icons.Filled.AccessTime, "Zeiterfassung", "Zeiten je Mitarbeiter", "adminTimeEntries"))
|
||||||
if (allowed("manage_escalations")) add(MoreEntry(Icons.AutoMirrored.Filled.Rule, "Eskalationsregeln", "Regeln für überfällige Tickets", "adminEscalation"))
|
if (allowed("manage_escalations")) add(MoreEntry(Icons.AutoMirrored.Filled.Rule, "Eskalationsregeln", "Regeln für überfällige Tickets", "adminEscalation"))
|
||||||
if (allowed("manage_business_hours")) add(MoreEntry(Icons.Filled.Schedule, "Geschäftszeiten", "SLA-Zeitfenster, Feiertage", "adminBusinessHours"))
|
if (allowed("manage_business_hours")) add(MoreEntry(Icons.Filled.Schedule, "Geschäftszeiten", "SLA-Zeitfenster, Feiertage", "adminBusinessHours"))
|
||||||
if (isFullAdmin) add(MoreEntry(Icons.Filled.Bolt, "Automatisierung", "Ticket-Regeln, Auto-Antworten", "adminTicketRules"))
|
if (allowed("manage_automation")) add(MoreEntry(Icons.Filled.Bolt, "Automatisierung", "Ticket-Regeln, Auto-Antworten", "adminTicketRules"))
|
||||||
if (isFullAdmin) add(MoreEntry(Icons.Filled.Block, "Sperrliste", "Absender sperren", "adminBanlist"))
|
if (allowed("manage_banlist")) add(MoreEntry(Icons.Filled.Block, "Sperrliste", "Absender sperren", "adminBanlist"))
|
||||||
add(MoreEntry(Icons.Filled.Flag, "Statusseite", "Dienste und Störungen für Kunden", "adminStatus"))
|
add(MoreEntry(Icons.Filled.Flag, "Statusseite", "Dienste und Störungen für Kunden", "adminStatus"))
|
||||||
if (allowed("view_audit_log")) add(MoreEntry(Icons.Filled.History, "Admin-Protokoll", "Änderungen und Mailfehler", "adminAuditLog"))
|
if (allowed("view_audit_log")) add(MoreEntry(Icons.Filled.History, "Admin-Protokoll", "Änderungen und Mailfehler", "adminAuditLog"))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user