From 19bfd99e6349a98363d73fa65f566a99d55cefac Mon Sep 17 00:00:00 2001 From: M_Viper Date: Sat, 24 Oct 2026 20:16:47 +0000 Subject: [PATCH] Upload via GUI (80 Dateien) --- mobile/src/main/assets/lang_en.json | 1 + .../java/de/deskhub/staff/DeskHubWidget.kt | 5 +- .../java/de/deskhub/staff/MainActivity.kt | 1 + .../de/deskhub/staff/data/TicketRepository.kt | 12 +- .../deskhub/staff/data/remote/DeskHubApi.kt | 20 ++ .../deskhub/staff/data/remote/dto/ApiDtos.kt | 4 + .../staff/data/remote/dto/ExtraDtos.kt | 118 ++++++++++ .../deskhub/staff/domain/model/ExtraModels.kt | 7 + .../de/deskhub/staff/domain/model/Models.kt | 4 + .../de/deskhub/staff/ui/AssistantAdmin.kt | 3 +- .../java/de/deskhub/staff/ui/DeskHubApp.kt | 109 ++++++++- .../de/deskhub/staff/ui/DeskHubViewModel.kt | 69 ++++++ .../main/java/de/deskhub/staff/ui/DeskNav.kt | 3 +- .../java/de/deskhub/staff/ui/DeskTheme.kt | 1 - .../src/main/java/de/deskhub/staff/ui/I18n.kt | 152 ++++++++++++ .../main/java/de/deskhub/staff/ui/I18nText.kt | 81 +++++++ .../de/deskhub/staff/ui/PushPrefsScreen.kt | 1 - .../de/deskhub/staff/ui/StatusPageAdmin.kt | 220 ++++++++++++++++++ .../res/drawable/ic_launcher_foreground.xml | 28 +-- .../res/drawable/ic_launcher_monochrome.xml | 12 + mobile/src/main/res/layout/widget_deskhub.xml | 10 +- .../main/res/layout/widget_deskhub_large.xml | 20 +- .../main/res/layout/widget_deskhub_small.xml | 6 +- .../res/mipmap-anydpi-v26/ic_launcher.xml | 1 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 1 + mobile/src/main/res/values-en/strings.xml | 14 ++ mobile/src/main/res/values/strings.xml | 21 +- 27 files changed, 865 insertions(+), 59 deletions(-) create mode 100644 mobile/src/main/assets/lang_en.json create mode 100644 mobile/src/main/java/de/deskhub/staff/data/remote/dto/ExtraDtos.kt create mode 100644 mobile/src/main/java/de/deskhub/staff/domain/model/ExtraModels.kt create mode 100644 mobile/src/main/java/de/deskhub/staff/ui/I18n.kt create mode 100644 mobile/src/main/java/de/deskhub/staff/ui/I18nText.kt create mode 100644 mobile/src/main/java/de/deskhub/staff/ui/StatusPageAdmin.kt create mode 100644 mobile/src/main/res/drawable/ic_launcher_monochrome.xml create mode 100644 mobile/src/main/res/values-en/strings.xml diff --git a/mobile/src/main/assets/lang_en.json b/mobile/src/main/assets/lang_en.json new file mode 100644 index 00000000..f1389c52 --- /dev/null +++ b/mobile/src/main/assets/lang_en.json @@ -0,0 +1 @@ +{"%.1f Std":"%.1f hrs","(Gast)":"(Guest)","(zusätzlich)":"(additional)","+ Aktion":"+ Action","+ Aktion hinzufügen":"+ Add action","+ Antwort hinzufügen":"+ Add answer","+ Aufgabe":"+ Task","+ Bedingung":"+ Condition","+ Bedingung hinzufügen":"+ Add condition","+ Erfassen":"+ Log time","+ Kategorie hinzufügen":"+ Add category","+ Neu":"+ New","+ Neue Aufgabe":"+ New task","+ Neue Regel":"+ New rule","+ Neuer Artikel":"+ New article","+ Neuer Textbaustein":"+ New text snippet","+ Neues Muster":"+ New pattern","+ Neues Ticket":"+ New ticket","+ Ticket erstellen":"+ Create ticket","+ hinzufügen":"+ add","0 Gerät(e) angemeldet":"0 device(s) signed in","1 Std":"1 hr","1 Tag":"1 day","1 Treffer ·":"1 hit ·","1 Woche":"1 week","10 Backups behalten":"Keep 10 backups","14 Backups behalten":"Keep 14 backups","2 Std":"2 hrs","2 Tage":"2 days","3 Backups behalten":"Keep 3 backups","3 Std":"3 hrs","3 Tage":"3 days","30 Backups behalten":"Keep 30 backups","5 Backups behalten":"Keep 5 backups","6-stelliger Code":"6-digit code","7 Backups behalten":"Keep 7 backups","7 Tage":"7 days","8 Std":"8 hrs",": bitte in der App erneut versuchen":": please try again in the app","API-Schlüssel":"API keys","Abbrechen":"Cancel","Abfrage am {} gesendet, noch keine Antwort.":"Survey sent on {}, no answer yet.","Abfrage-Intervall":"Fetch interval","Abgehend (SMTP)":"Outgoing (SMTP)","Abgehend (eigener SMTP-Server)":"Outgoing (own SMTP server)","Abmelden":"Sign out","Abmelden / Server wechseln":"Sign out / switch server","Abruf":"Fetch","Abruf aktivieren":"Enable fetching","Abruf für diese Adresse aktivieren":"Enable fetching for this address","Absender":"Sender","Absender oder Mailanbieter sperren":"Block sender or mail provider","Absender sperren":"Block sender","Absender sperren…":"Block sender…","Absender-E-Mail":"Sender email","Absender-Name":"Sender name","Absender:":"Sender:","Absendername":"Sender name","Abteilung":"Department","Abteilung aktualisieren":"Update department","Abteilung für Live-Chat-Tickets":"Department for live chat tickets","Abteilung für Tickets des Assistenten":"Department for assistant tickets","Abteilung hinzufügen":"Add department","Abteilung nicht gefunden.":"Department not found.","Abteilung setzen":"Set department","Abteilung und Priorität stammen aus dem Reiter „Konto“.":"Department and priority come from the \"Account\" tab.","Abteilung wirklich löschen?":"Really delete this department?","Abteilung ändern":"Change department","Abteilung „":"Department \"","Abteilung „Allgemeiner Support“ (nicht hinterlegt)":"Department \"General support\" (not set)","Abteilung/Priorität bestimmt die Zeiten":"Department/priority determines the times","Abteilung:":"Department:","Abteilungen":"Departments","Abteilungen verwalten":"Manage departments","Abteilungen werden geladen …":"Loading departments …","Abteilungs-E-Mail (optional)":"Department email (optional)","Abteilungs-E-Mail (optional, sonst Standardadresse)":"Department email (optional, otherwise default address)","Abteilungs-Routing-Tabelle":"Department routing table","Abteilungsinformationen":"Department information","Abteilungsleiter":"Department head","Achtung:":"Attention:","Admin benachrichtigen":"Notify admin","Admin-Protokoll":"Admin log","Admin-Protokoll einsehen":"View admin log","Administrator (alle Rechte)":"Administrator (all rights)","Administratornotizen":"Administrator notes","Administratornotizen (intern)":"Administrator notes (internal)","Adminportal":"Admin portal","Adminportal-Bereiche (\"Co-Admin\")":"Admin portal areas (\"co-admin\")","Admins benachrichtigen":"Notify admins","Adresse für Kunden:":"Address for customers:","Adresse für Kunden: {}":"Address for customers: {}","Adressen und Postfach-Abruf":"Addresses and mailbox fetching","Adressen verwaltest du unter „E-Mail-Adressen“.":"You manage addresses under \"Email addresses\".","Adressen, Priorität und Postfach-Abruf pflegst du unter":"You manage addresses, priority and mailbox fetching under","Agent anlegen":"Create agent","Agenten":"Agents","Agenten verwalten — {}":"Manage agents — {}","Aktion":"Action","Aktion nach Abruf":"Action after fetch","Aktionen":"Actions","Aktiv":"Active","Aktiv (auf dem Ticket-Formular sichtbar)":"Active (visible on the ticket form)","Aktiv (deaktivierte Rolle wird ignoriert)":"Active (a disabled role is ignored)","Aktiv (sonst deaktiviert)":"Active (otherwise disabled)","Aktiv (sonst gesperrt)":"Active (otherwise blocked)","Aktiv (wird beim Abruf berücksichtigt)":"Active (used when fetching)","Aktive Live-Chats (0)":"Active live chats (0)","Aktivieren":"Activate","Aktivität":"Activity","Aktualisiere …":"Refreshing …","Aktualisieren":"Refresh","Aktualisiert":"Updated","Aktuell keine offenen Live-Chats.":"There are currently no open live chats.","Aktuell würden":"Currently","Aktuelle Filter als Ansicht speichern":"Save current filters as a view","Aktuelle Filter speichern …":"Save current filters …","Aktuelles Passwort":"Current password","Alle":"All","Alle Absender der markierten Tickets sperren und Tickets schließen?":"Block all senders of the selected tickets and close the tickets?","Alle Abteilungen":"All departments","Alle Artikel durchsuchen":"Browse all articles","Alle Bedingungen":"All conditions","Alle Bedingungen müssen zutreffen":"All conditions must match","Alle Ergebnisse":"All results","Alle Prioritäten":"All priorities","Alle Status":"All statuses","Alle Tags":"All tags","Alle Textbausteine":"All text snippets","Alle Themen (oder Formular ohne Themenauswahl)":"All topics (or form without topic selection)","Alle Zuständigkeiten":"All assignments","Alle aktiven":"All active","Alle anlegen":"Create all","Alle außer Geschlossen":"All except closed","Alle behalten":"Keep all","Alle bekannten Datenbank-Änderungen sind eingespielt.":"All known database changes have been applied.","Alle eingegebenen Adressen sperren und Tickets schließen?":"Block all entered addresses and close tickets?","Alle löschen":"Delete all","Alle sperren":"Block all","Allgemein":"General","Allgemeine Anfrage":"General inquiry","Allgemeiner Support":"General support","Alphabetisch":"Alphabetical","Als Spam markiert":"Marked as spam","Als gelesen markieren":"Mark as read","An diesen Tagen laufen keine SLA-Fristen, unabhängig vom Wochentag.":"No SLA deadlines run on these days, regardless of the weekday.","Analysieren":"Analyze","Android fragt einmalig, ob der Browser Apps installieren darf. Das bitte erlauben.":"Android asks once whether the browser may install apps. Please allow it.","Anfrage-Quelle":"Request source","Angaben des Kunden":"Customer details","Angepasst":"Customized","Angewendet":"Applied","Anhang":"Attachment","Anhänge (optional, mehrere möglich)":"Attachments (optional, multiple allowed)","Anmelden":"Sign in","Anmelden – {}":"Sign in – {}","Anmeldung für Support-Mitarbeiter und Administratoren.":"Sign-in for support staff and administrators.","Anmeldung wird geprüft …":"Checking sign-in …","Anonymisiert":"Anonymized","Anonymisierung jetzt ausführen? Dies kann nicht rückgängig gemacht werden.":"Run anonymization now? This cannot be undone.","Anordnung der Optionen":"Order of the options","Ansicht aktualisieren":"Refresh view","Ansicht gespeichert":"View saved","Ansicht löschen":"Delete view","Ansicht löschen?":"Delete view?","Ansicht speichern":"Save view","Ansicht umbenennen":"Rename view","Ansichten":"Views","Antwort":"Answer","Antwort *":"Answer *","Antwort / Ticket":"Answer / ticket","Antwort an Kunden":"Reply to customer","Antwort anlegen":"Create answer","Antwort bearbeiten":"Edit answer","Antwort dazu anlegen":"Create an answer for it","Antwort gelöscht":"Answer deleted","Antwort gesendet":"Reply sent","Antwort gespeichert":"Answer saved","Antwort jetzt an den Kunden senden?":"Send the reply to the customer now?","Antwort löschen":"Delete answer","Antwort nicht gesendet":"Reply not sent","Antwort schreiben":"Write reply","Antwort senden":"Send reply","Antworten":"Replies","Antworten / Notiz":"Reply / note","Antworten auf häufige Fragen – oft schneller als der Weg über ein Ticket.":"Answers to common questions – often faster than opening a ticket.","Antworten, Fragen-Log, Einstellungen":"Answers, question log, settings","Antworten, Fragen-Log, Einstellungen, Synonyme":"Answers, question log, settings, synonyms","Antwort…":"Answer…","Anzeigename":"Display name","Anzeigename & URL":"Display name & URL","Anzeigename (optional)":"Display name (optional)","App öffnen, Serveradresse eingeben und anmelden.":"Open the app, enter the server address and sign in.","Apps für Mitarbeiter":"Apps for staff","Artikel":"Article","Artikel bearbeiten":"Edit article","Artikel und Kategorien":"Articles and categories","Artikel wirklich löschen?":"Really delete this article?","Auch Nachrichten durchsuchen":"Also search messages","Auf Standard zurücksetzen?":"Reset to default?","Auf später legen":"Snooze","Auf später legen: das Ticket verschwindet aus den Listen und kommt zurück, mit Benachrichtigung.":"Snooze: the ticket disappears from the lists and comes back, with a notification.","Auf „Alle Themen“ belassen, damit das Feld unabhängig vom gewählten Anliegen erscheint.":"Leave on \"All topics\" so the field appears regardless of the chosen request.","Aufbewahrungsdauer nach Schließung":"Retention period after closing","Aufgabe":"Task","Aufgabe löschen?":"Delete task?","Aufgaben":"Tasks","Aufgaben werden geladen …":"Loading tasks …","Aufrufe":"Views","Aus":"Off","Ausfall":"Outage","Auslöser":"Trigger","Ausprobieren":"Try out","Auswahl aufheben":"Clear selection","Auswahl:":"Selection:","Auswertungen":"Reports","Authenticator-Code":"Authenticator code","Auto-Antwort senden (Textbaustein)":"Send auto-reply (text snippet)","Automatisch":"Automatic","Automatisch (Routing-Tabelle / globale Standardabteilung)":"Automatic (routing table / global default department)","Automatisch (Standardadresse der Abteilung)":"Automatic (department default address)","Automatisch offline nach":"Go offline automatically after","Automatisch schließen":"Close automatically","Automatische Anonymisierung aktivieren":"Enable automatic anonymization","Automatische Backups":"Automatic backups","Automatische Backups jetzt manuell ausführen?":"Run automatic backups manually now?","Automatische Datenlöschung":"Automatic data deletion","Automatische Zeiterfassung (misst automatisch, wie lange ein Mitarbeiter ein Ticket geöffnet hat)":"Automatic time tracking (measures how long a staff member has a ticket open)","Automatischen Hintergrund-Betrieb aktiv halten":"Keep automatic background operation active","Automatischer Logout nach Inaktivität":"Automatic logout after inactivity","Automatisches Schließen":"Automatic closing","Automatisches Schließen jetzt manuell ausführen?":"Run automatic closing manually now?","Automatisierung":"Automation","Automatisierung und Auto-Antworten":"Automation and auto-replies","Backup-Intervall":"Backup interval","Backup-Rotation (lokale Kopien behalten)":"Backup rotation (local copies to keep)","Backups enthalten alle Dateien inkl.":"Backups contain all files incl.","Backups liegen in":"Backups are stored in","Backups vorhanden":"Backups available","Backups werden automatisch vor jedem Update erstellt":"Backups are created automatically before every update","Backups werden nach der Erstellung automatisch per FTP übertragen.":"Backups are transferred automatically via FTP after creation.","Bald fällig":"Due soon","Bayern":"Bavaria","Beantwortet":"Answered","Beantwortete Tickets nach":"Answered tickets after","Bearbeiten":"Edit","Bearbeitung":"Processing","Bedingungen":"Conditions","Bedingungen (optional – leer = alle Tickets)":"Conditions (optional – empty = all tickets)","Beenden":"Quit","Begrüßung":"Greeting","Behoben":"Resolved","Bei einer":"For a","Bei neuen Aktualisierungen an deinen Tickets und neuen, nicht zugewiesenen Tickets.":"On new updates to your tickets and new unassigned tickets.","Bei schweren Fehlern auf der Webseite eine E-Mail senden":"Send an email on serious errors on the website","Beide leer = immer Popups. Es gilt die Zeit des Servers, auch über Mitternacht (z. B. 22:00 bis 06:00).":"Both empty = always popups. The server time applies, also past midnight (e.g. 22:00 to 06:00).","Beim Schließen im Infobereich weiterlaufen":"Keep running in the notification area when closing","Beim Öffnen automatisch zuweisen (ein noch nicht zugewiesenes Ticket geht an den Mitarbeiter, der es öffnet)":"Assign automatically on open (an unassigned ticket goes to the staff member who opens it)","Beliebig":"Any","Beliebte Artikel":"Popular articles","Benachr.":"Notif.","Benachrichtigung bei Ticketzuweisung deaktivieren":"Disable notification on ticket assignment","Benachrichtigungen":"Notifications","Benannte Zeiten je Abteilung oder Thema":"Named times per department or topic","Benutzerdefiniert":"Custom","Benutzerdefinierte Berechtigungssets für Mitarbeiter. Ohne Rolle gelten die Standard-Mitarbeiterrechte.":"Custom permission sets for staff. Without a role, the default staff permissions apply.","Benutzerdefinierte Listen":"Custom lists","Benutzername":"Username","Benutzername (leer = E-Mail-Adresse)":"Username (empty = email address)","Benutzername / E-Mail":"Username / email","Beobachter":"Watchers","Beobachter und Cc":"Watchers and Cc","Beobachtetes Ticket aktualisiert":"Watched ticket updated","Berechtigungen":"Permissions","Berechtigungssets":"Permission sets","Bereich":"Area","Bereich „Beliebte Artikel“ auf der Startseite anzeigen":"Show the \"Popular articles\" section on the home page","Bereits registriert?":"Already registered?","Bericht erstellen":"Create report","Bericht gespeichert":"Report saved","Bericht löschen?":"Delete report?","Bericht speichern":"Save report","Bericht-Baukasten":"Report builder","Berichte":"Reports","Berichte ansehen":"View reports","Berichte einsehen":"View reports","Berichtszeitraum":"Reporting period","Beschreiben Sie Ihr Anliegen so detailliert wie möglich…":"Describe your request in as much detail as possible…","Beschreiben Sie Ihr Anliegen so genau wie möglich – das hilft uns, schneller zu helfen.":"Describe your request as precisely as possible – it helps us help you faster.","Beschreibung":"Description","Beschreibung (optional)":"Description (optional)","Beschriftung im Footer":"Footer label","Beschädigte Sitzungsdaten konnten nicht entfernt werden.":"Corrupted session data could not be removed.","Beschäftigt":"Busy","Bestätigen":"Confirm","Betreff":"Subject","Betreff & letzte Nachricht":"Subject & last message","Betreff und Kundenname in der Benachrichtigung anzeigen":"Show subject and customer name in the notification","Betreff · Ticket-Nr. · Kundenname · E-Mail":"Subject · ticket no. · customer name · email","Betrifft":"Affects","Betrifft: {}":"Affects: {}","Bewertet am":"Rated on","Bewertet am {}":"Rated on {}","Bezeichnung":"Label","Bezeichnung (z. B. Neujahr)":"Label (e.g. New Year)","Bild aus der Signatur":"Image from signature","Bild wird hochgeladen …":"Uploading image …","Bis":"To","Bis (HH:MM)":"Until (HH:MM)","Bis (JJJJ-MM-TT)":"Until (YYYY-MM-DD)","Bitte Authenticator-Code eingeben.":"Please enter the authenticator code.","Bitte Benachrichtigungen im Browser erlauben.":"Please allow notifications in the browser.","Bitte eine Datei auswaehlen.":"Please select a file.","Bitte eine Ziel-Abteilung wählen.":"Please choose a target department.","Bitte einen Zeitpunkt in der Zukunft wählen.":"Please choose a point in time in the future.","Bitte in der App anmelden.":"Please sign in in the app.","Bitte in der App neu anmelden.":"Please sign in again in the app.","Bitte mindestens ein Ticket markieren.":"Please select at least one ticket.","Bitte schildern Sie uns so viele Details wie möglich, damit wir Ihnen bestmöglich helfen können.":"Please give us as many details as possible so we can help you in the best way.","Bitte warten …":"Please wait …","Browser-Push-Benachrichtigungen":"Browser push notifications","Buchhaltung":"Accounting","Bundesland":"Federal state","Bündeln Sie mehrere Kundenkonten unter einer Firma/Organisation. Kontakte werden unter „Kunden“ zugeordnet.":"Group several customer accounts under one company/organization. Contacts are assigned under \"Customers\".","CSV exportieren":"Export CSV","Cc entfernen":"Remove Cc","Cc hinzufügen":"Add Cc","Cc hinzugefügt":"Cc added","Cc-Adresse hinzufügen":"Add Cc address","Cc-Empfänger":"Cc recipients","Chat auswählen":"Choose chat","Chat beenden":"End chat","Chat beenden?":"End chat?","Chat starten":"Start chat","Chat wird geladen …":"Loading chat …","Chat wirklich beenden?":"Really end chat?","Chats werden geladen …":"Loading chats …","Chatverlauf (an Kunde, nach Chatende)":"Chat transcript (to customer, after chat ends)","Chatverlauf per E-Mail an den Kunden senden, sobald ein Mitarbeiter den Chat beendet":"Send the chat transcript to the customer by email as soon as a staff member ends the chat","Checkbox (Ja/Nein)":"Checkbox (yes/no)","Copyright © {} – Alle Rechte vorbehalten.":"Copyright © {} – All rights reserved.","Cron-Job einrichten:":"Set up cron job:","Cron-Jobs":"Cron jobs","Cron-Status":"Cron status","Crontab-Eintrag":"Crontab entry","Crontab-Zeilen anzeigen":"Show crontab lines","Da gerade niemand direkt verfügbar ist, antworten wir Ihnen per E-Mail, sobald wir Ihre Anfrage bearbeitet haben.":"Nobody is available right now, so we will reply by email as soon as we have looked into your request.","Dann …":"Then …","Darf Tickets anlegen":"May create tickets","Darf Ticketstatus lesen (mit Ticketnummer + E-Mail des Anfragenden)":"May read ticket status (with ticket number + requester's email)","Darstellung":"Appearance","Das Ticket verschwindet aus den Listen und kommt zum gewählten Zeitpunkt zurück, mit Benachrichtigung. Antwortet der Kunde früher, kommt es sofort zurück.":"The ticket disappears from the lists and comes back at the chosen time, with a notification. If the customer replies earlier, it comes back immediately.","Das Ticket wird angelegt und gleich geschlossen. Es gibt keine Benachrichtigung an Mitarbeiter oder Kunden.":"The ticket is created and closed immediately. Neither staff nor customers are notified.","Das Token hat kein Ablaufdatum.":"The token has no expiry date.","Das gesamte Fragen-Log löschen?":"Delete the entire question log?","Das gesamte Fragen-Log wird gelöscht.":"The entire question log will be deleted.","Das sind deutsche Texte, die der Sammelmodus gesehen hat. Sie bilden die Vorlage für ein neues Sprachpaket.":"These are the German texts the collection mode has seen. They form the template for a new language pack.","Datei auf dem Handy herunterladen und öffnen.":"Download the file on your phone and open it.","Datei auswählen":"Choose file","Datei herunterladen und starten.":"Download the file and run it.","Datei per FTP in den Ordner":"Upload the file via FTP into the folder","Datenanonymisierung":"Data anonymization","Datenanonymisierung jetzt manuell ausführen?":"Run data anonymization manually now?","Datenbank":"Database","Datenschutz":"Privacy","Datenschutz & Impressum":"Privacy & legal notice","Datum":"Date","Datum (JJJJ-MM-TT)":"Date (YYYY-MM-DD)","Dazu passend":"Related","Deaktivieren":"Deactivate","Deaktiviert":"Disabled","Der Agent ist Mitglied in folgenden Teams.":"The agent is a member of the following teams.","Der Assistent beantwortet Fragen von Kunden aus der Wissensdatenbank und aus den zusätzlichen Antworten. Findet er nichts, bietet er ein Ticket an. Es wird kein externer KI-Dienst verwendet.":"The assistant answers customer questions from the knowledge base and the additional answers. If it finds nothing, it offers a ticket. No external AI service is used.","Der Assistent beantwortet Fragen von Kunden aus der Wissensdatenbank und den zusätzlichen Antworten. Findet er nichts, bietet er ein Ticket an. Es wird kein externer KI-Dienst verwendet.":"The assistant answers customer questions from the knowledge base and the additional answers. If it finds nothing, it offers a ticket. No external AI service is used.","Der Branding-Rahmen (Kopf-/Fußzeile) wird beim Versand automatisch ergänzt – hier nur den Inhalt anpassen.":"The branding frame (header/footer) is added automatically when sending – only adjust the content here.","Der Katalog konnte nicht geladen werden: SSL certificate problem: unable to get local issuer certificate":"The catalog could not be loaded: SSL certificate problem: unable to get local issuer certificate","Der Ordner install/ existiert ohne Sperrdatei. Löschen Sie den Ordner install/ vom Server.":"The install/ folder exists without a lock file. Delete the install/ folder from the server.","Der Server hat keine Antwort geliefert.":"The server did not return a response.","Design wechseln":"Switch design","DeskHub Desktop {} · Rechte und Daten kommen vom Server – genau wie in der Android-App.":"DeskHub Desktop {} · Permissions and data come from the server – just like in the Android app.","DeskHub Ticketsystem":"DeskHub ticket system","Deutsch":"German","Die":"The","Die Aktion ist fehlgeschlagen.":"The action failed.","Die Anmeldung lieferte kein Mitarbeiterprofil.":"The sign-in returned no staff profile.","Die Anmeldung lieferte kein Token.":"The sign-in returned no token.","Die Datenbank ist noch nicht aktuell. Bitte im Adminportal unter System-Update „Datenbank aktualisieren“ klicken.":"The database is not up to date yet. Please click \"Update database\" in the admin portal under System update.","Die Liste der Versionen konnte nicht geladen werden: API HTTP 0":"The list of versions could not be loaded: API HTTP 0","Die Seite konnte nicht geöffnet werden.":"The page could not be opened.","Die Server-Adresse ist ungültig.":"The server address is invalid.","Die Server-Adresse muss HTTPS verwenden.":"The server address must use HTTPS.","Die Serverantwort ist ungültig.":"The server response is invalid.","Die angeforderte Seite existiert nicht.":"The requested page does not exist.","Die geschützte Anmeldung konnte nicht gelesen werden und wurde entfernt.":"The protected sign-in could not be read and was removed.","Die geschützte Anmeldung konnte nicht gespeichert werden.":"The protected sign-in could not be saved.","Die lokale Anmeldung konnte nicht entfernt werden.":"The local sign-in could not be removed.","Die meistgelesenen Antworten aus unserer Wissensdatenbank.":"The most-read answers from our knowledge base.","Dienst entfernen?":"Remove service?","Dienst setzen auf":"Set service to","Dienstag":"Tuesday","Dienste":"Services","Dienste und Störungen für Kunden":"Services and incidents for customers","Dienste und Störungsmeldungen für Kunden":"Services and incident messages for customers","Diese Adresse gebt ihr beim ersten Start der App ein, danach meldet ihr euch mit eurem Mitarbeiter-Konto an.":"You enter this address when you first start the app, then sign in with your staff account.","Diese Antworten haben Vorrang vor der Wissensdatenbank. Der Assistent findet sie über die Wörter in der Frage und in den Stichwörtern.":"These answers take precedence over the knowledge base. The assistant finds them through the words in the question and in the keywords.","Diesen Bericht speichern":"Save this report","Dieser Abteilung sind noch keine Mitarbeiter zugewiesen. Die Zuweisung erfolgt unter „Mitarbeiter“.":"No staff members are assigned to this department yet. Assignment is done under \"Staff\".","Dieser Bewertungslink ist ungültig oder abgelaufen.":"This rating link is invalid or has expired.","Dieser Chat wird gerade von „{}“ bearbeitet – nur Ansicht.":"This chat is currently being handled by \"{}\" – view only.","Dieser Chat wurde beendet.":"This chat has ended.","Dieser Monat":"This month","Dieses Jahr":"This year","Dieses Ticket":"This ticket","Dieses Ticket wurde nicht gefunden.":"This ticket was not found.","Digitalen Assistenten einschalten":"Enable digital assistant","Digitaler Assistent":"Digital assistant","Dir fehlt das Recht „Tickets erstellen“.":"You lack the \"Create tickets\" permission.","Dir zugewiesen":"Assigned to you","Discord Webhook-URL":"Discord webhook URL","Domain (ganzer Mailanbieter)":"Domain (whole mail provider)","Domain, z.B. spam.de":"Domain, e.g. spam.com","Donnerstag":"Thursday","Dringend":"Urgent","Dropdown-Auswahl":"Dropdown selection","Druckdatum: 24.10.2026 19:44":"Print date: 24.10.2026 19:44","Druckdatum: {}":"Print date: {}","Du beobachtest das Ticket jetzt":"You are now watching the ticket","Du beobachtest das Ticket nicht mehr":"You are no longer watching the ticket","Dunkel":"Dark","E-Mail":"Email","E-Mail (optional)":"Email (optional)","E-Mail / Domain":"Email / domain","E-Mail des Kunden (optional)":"Customer email (optional)","E-Mail, z.B. spam@beispiel.de":"Email, e.g. spam@example.com","E-Mail-Abruf":"Email fetching","E-Mail-Abruf (IMAP)":"Email fetching (IMAP)","E-Mail-Abruf jetzt manuell ausführen?":"Run email fetching manually now?","E-Mail-Adresse":"Email address","E-Mail-Adresse *":"Email address *","E-Mail-Adresse bearbeiten":"Edit email address","E-Mail-Adresse oder Passwort ist falsch.":"Email address or password is incorrect.","E-Mail-Adresse wirklich löschen?":"Really delete this email address?","E-Mail-Adressen":"Email addresses","E-Mail-Adressen verwalten":"Manage email addresses","E-Mail-Adressen werden geladen …":"Loading email addresses …","E-Mail-Versand aktiv (PHP mail() Fallback)":"Email sending active (PHP mail() fallback)","E-Mail-Vorlagen":"Email templates","E-Mail-Vorlagen verwalten":"Manage email templates","E-Mails / Domains":"Emails / domains","Echter Server-Cronjob":"Real server cron job","Eigene Inhaltsseiten für das Kundenportal (z. B. Impressum, Datenschutz, Willkommen). Erreichbar unter":"Custom content pages for the customer portal (e.g. legal notice, privacy, welcome). Available at","Eigene Kundenportal-Themes":"Custom customer portal themes","Eigene Sprache als Plugin erstellen":"Create your own language as a plugin","Eigenen SMTP-Server für diese Adresse verwenden":"Use own SMTP server for this address","Eigenen SMTP-Server verwenden":"Use own SMTP server","Eigener SMTP":"Own SMTP","Eigenes Plugin hochladen":"Upload your own plugin","Eigenschaften":"Properties","Ein von dir gesetzter Status bleibt, bis der Mitarbeiter ihn selbst ändert oder du ihn erneut änderst.":"A status you set stays until the staff member changes it or you change it again.","Eine Adresse pro Zeile, optional mit Anzeigename nach einem Komma (z. B.":"One address per line, optionally with a display name after a comma (e.g.","Eine Bedingung":"One condition","Eine E-Mail oder Domain pro Zeile (oder kommagetrennt). Typ wird automatisch erkannt.":"One email or domain per line (or comma-separated). The type is detected automatically.","Eine E-Mail oder Domain pro Zeile. Typ wird automatisch erkannt.":"One email or domain per line. The type is detected automatically.","Eingang":"Inbox","Eingangsbestätigung per E-Mail an Kunden senden (nur wenn E-Mail angegeben)":"Send an acknowledgement of receipt to the customer by email (only if an email address was given)","Eingangsbestätigung per E-Mail senden":"Send acknowledgement of receipt by email","Eingeschränkt":"Degraded","Einstellungen":"Settings","Einstellungen bei neuem Ticket":"Settings for new tickets","Einstellungen konnten nicht geladen werden (hat der Server die neueste Version?).":"Settings could not be loaded (does the server have the latest version?).","Einträge":"Entries","Einzeiliger Text":"Single-line text","Einzeln":"Individually","Einzeln sperren":"Block individually","Empfänger (leer = Administratoren), mehrere mit Komma":"Recipients (empty = administrators), several separated by comma","Entfernen":"Remove","Entferntes Postfach":"Remote mailbox","Entferntes Postfach (Abruf per IMAP)":"Remote mailbox (fetch via IMAP)","Entsperren":"Unblock","Entwurf":"Draft","Entwürfe gelöscht":"Drafts deleted","Er bleibt als Ticket erhalten, verschwindet aber aus den aktiven Live-Chats.":"It remains as a ticket but disappears from the active live chats.","Ereignisse":"Events","Erfasste Arbeitszeiten":"Recorded working times","Ergebnis":"Result","Erhalten eine Kopie jeder öffentlichen Antwort per E-Mail.":"Receive a copy of every public reply by email.","Erinnerung (an Kunde, wenn er nicht reagiert)":"Reminder (to customer, if they do not respond)","Erlaubte Datei-Endungen":"Allowed file extensions","Erlaubte Dateitypen":"Allowed file types","Erlaubte IP-Adressen (optional)":"Allowed IP addresses (optional)","Erlaubte IPs":"Allowed IPs","Erledigt":"Done","Erneut":"Again","Erneut versuchen":"Try again","Ersetzt die Zeiten der Priorität für Tickets, deren Hilfethema oder Abteilung diesen Plan nutzt (Hilfethema vor Abteilung).":"Replaces the priority's times for tickets whose help topic or department uses this plan (help topic before department).","Erste Schritte":"Getting started","Erstellen":"Create","Erstellen Sie ein Kundenkonto, um Tickets zu verfolgen.":"Create a customer account to track your tickets.","Erstellen Sie unter Backup eines und prüfen Sie es.":"Create one under Backup and check it.","Erstellt":"Created","Erstellt:":"Created:","Erstreaktion":"First response","Erwartete Prüfsumme (SHA-256, optional)":"Expected checksum (SHA-256, optional)","Erweiterter Zugriff":"Extended access","Erwähnungen":"Mentions","Es sind noch keine Teams angelegt.":"No teams have been created yet.","Es sind noch keine Teams angelegt. Teams legst du unter":"No teams have been created yet. You create teams under","Es wird keine Signatur angehängt.":"No signature will be attached.","Eskalationsregeln":"Escalation rules","Eskalationsregeln jetzt manuell ausführen?":"Run escalation rules manually now?","Eskalationsregeln verwalten":"Manage escalation rules","Eskalationsregeln werden stündlich per Cron geprüft (":"Escalation rules are checked hourly via cron (","Exportieren (CSV)":"Export (CSV)","Externer Link":"External link","Externer Link (optional)":"External link (optional)","FTP speichern":"Save FTP","FTP-Host":"FTP host","FTP-Rotation (Kopien auf Server)":"FTP rotation (copies on server)","Falls Windows warnt („Der Computer wurde geschützt“):":"If Windows warns (\"Windows protected your PC\"):","Farbe (#RRGGBB)":"Color (#RRGGBB)","Favicon (Symbol im Browser-Tab)":"Favicon (icon in the browser tab)","Fehler:":"Error:","Feiertag hinzufügen":"Add holiday","Feiertage":"Holidays","Feiertage (0)":"Holidays (0)","Feiertage / Schließtage":"Holidays / closing days","Feiertage automatisch übernehmen":"Import holidays automatically","Feld":"Field","Feldtyp":"Field type","Fenster schließen":"Close window","Fertig":"Done","Fertige Designs direkt installieren. Prüfsumme und Signatur werden vor der Installation geprüft.":"Install ready-made designs directly. Checksum and signature are verified before installation.","Filter in der":"Filter in the","Filter zurücksetzen":"Reset filters","Filtern":"Filter","Firmenlogo":"Company logo","Formular":"Form","Frage":"Question","Frage / Thema":"Question / topic","Frage / Thema *":"Question / topic *","Frage ausprobieren":"Try a question","Fragen-Log":"Question log","Fragen-Log geleert":"Question log cleared","Fragen-Log leeren?":"Clear question log?","Fragen-Log: Aufbewahrung in Tagen (0 = nichts speichern)":"Question log: retention in days (0 = store nothing)","Frei konfigurierbare Auswertungen nach Erstellungsdatum der Tickets":"Freely configurable reports by ticket creation date","Freitag":"Friday","Fällig":"Due","Fällig bis":"Due until","Fällig bis (JJJJ-MM-TT HH:MM, optional)":"Due until (YYYY-MM-DD HH:MM, optional)","Für Kunden sichtbar (auf „Neues Ticket“)":"Visible to customers (on \"New ticket\")","Für alle Abteilungen":"For all departments","Für alle Mitarbeiter sichtbar":"Visible to all staff","Für automatische Backups den Cron einrichten:":"Set up cron for automatic backups:","Für dich":"For you","Gast":"Guest","Gast-Kunde":"Guest customer","Gast-Ticket zum Konto hinzufügen":"Add guest ticket to your account","Gedruckt: 24.10.2026 19:44":"Printed: 24.10.2026 19:44","Gedruckt: {}":"Printed: {}","Gefährliche Typen (PHP, EXE, SH etc.) werden immer blockiert – unabhängig von dieser Liste.":"Dangerous types (PHP, EXE, SH etc.) are always blocked – regardless of this list.","Gelöscht":"Deleted","Gelöst":"Resolved","Gelöst diesen Monat":"Resolved this month","Gelöst/Geschlossen":"Resolved/Closed","Genutzt":"Used","Geprüfte Plugins":"Verified plugins","Gerade ist niemand verfügbar – wir antworten aber, sobald wir wieder da sind.":"Nobody is available right now – we will reply as soon as we are back.","Gesammelte Texte löschen?":"Delete collected texts?","Gesammelte Texte: 556":"Collected texts: 556","Gesammelte Texte: {#}":"Collected texts: {}","Gesamt":"Total","Gesamt: {} Minuten":"Total: {} minutes","Gesamter Zeitraum":"Entire period","Gesamtgröße":"Total size","Geschlossen":"Closed","Geschäftszeiten":"Business hours","Geschäftszeiten für SLA-Berechnung verwenden":"Use business hours for SLA calculation","Geschäftszeiten verwalten":"Manage business hours","Geschäftszeiten werden geladen …":"Loading business hours …","Gespeichert":"Saved","Gespeichert ({} Gruppen)":"Saved ({} groups)","Gespeichert werden: assigned":"Saved: assigned","Gespeichert werden: keine Filter (alle Tickets)":"Saved: no filters (all tickets)","Gespeichert werden: status":"Saved: status","Gespeichert.":"Saved.","Gespeicherte Ansichten":"Saved views","Gespeicherte Berichte":"Saved reports","Gespeicherte Entwürfe löschen":"Delete saved drafts","Gespeicherte Zugangsdaten (SMTP, IMAP, Webhooks) sind mit einem eigenen Schlüssel verschlüsselt.":"Stored credentials (SMTP, IMAP, webhooks) are encrypted with a separate key.","Gesperrt":"Blocked","Gesperrt am":"Blocked on","Gesperrte Einträge (0)":"Blocked entries (0)","Geteilt von {}":"Shared by {}","Geöffnet":"Opened","Gilt für Hilfe-Thema":"Applies to help topic","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.":"Applies to tickets from the form, email, API and live chat. A ticket rule with \"Assign staff member\" takes precedence. On vacation, the substitute gets the ticket; \"Round robin\" skips staff on vacation or offline.","Gleichbedeutende Wörter":"Synonyms","Gleichbedeutende Wörter gespeichert":"Synonyms saved","Grund":"Reason","Grund (optional)":"Reason (optional)","Grund für alle (optional)":"Reason for all (optional)","Gruppe":"Group","Gruppieren nach":"Group by","Guten Abend":"Good evening","Guten Abend, {}":"Good evening, {}","Guten Morgen":"Good morning","Guten Morgen, {}":"Good morning, {}","Guten Tag":"Hello","Guten Tag, {}":"Hello, {}","HTML-Inhalt (Popup)":"HTML content (popup)","HTML-Inhalt (öffnet Popup). Ist ein Link gesetzt, wird das HTML-Feld ignoriert.":"HTML content (opens popup). If a link is set, the HTML field is ignored.","Handlungsbedarf ·":"Action needed ·","Hauptabteilung":"Main department","Hauptrolle":"Main role","Hell":"Light","Helpdesk-Status":"Helpdesk status","Herkunft":"Origin","Hessen":"Hesse","Heute":"Today","Heute gelöst":"Resolved today","Hier finden Sie das Archiv und den Verlauf Ihrer aktuellen und vorherigen Supportanfragen inklusive der Antworten.":"Here you can find the archive and history of your current and previous support requests, including the replies.","Hier finden Sie den Verlauf und Status Ihrer aktuellen und bisherigen Supportanfragen.":"Here you can find the history and status of your current and previous support requests.","Hier ist noch keine Android-App hinterlegt.":"No Android app has been stored here yet.","Hier ist noch keine Windows-App hinterlegt.":"No Windows app has been stored here yet.","Hier könnt ihr eigene Plugins als ZIP installieren oder aktualisieren, auch ohne Signatur. Diese Plugins sind":"Here you can install or update your own plugins as ZIP, even without a signature. These plugins are","Hilfe-Thema":"Help topic","Hilfe-Thema (optional)":"Help topic (optional)","Hilfe-Thema setzen":"Set help topic","Hilfe-Themen":"Help topics","Hilfe-Themen verwalten":"Manage help topics","Hilfethema":"Help topic","Hilfethema aktualisieren":"Update help topic","Hilfethema wirklich löschen?":"Really delete this help topic?","Hilfethemen":"Help topics","Hilfethemen werden geladen …":"Loading help topics …","Hilfethemeninformationen":"Help topic information","Hintergrundbild im dunklen Hero-Bereich. Mind. 1600×500 px, max. 5 MB.":"Background image in the dark hero area. At least 1600×500 px, max. 5 MB.","Hinweis für Administratoren: Zum Aktualisieren einer App die neue Datei per FTP in":"Note for administrators: to update an app, upload the new file via FTP to","Hinweis:":"Note:","Hinweise":"Notes","Hinweise ·":"Notes ·","Hinweistext (optional)":"Note text (optional)","Hinzufügen":"Add","Hoch":"High","Hochladen":"Upload","Hochladen & installieren":"Upload & install","Hochladen →":"Upload →","Hohe Priorität":"High priority","Häufige Fragen":"Frequently asked questions","Häufige Fragen ohne Antwort":"Frequent questions without an answer","IP-Adresse":"IP address","IP-Adressen sofort beim Speichern kürzen (letztes Oktett entfernen)":"Shorten IP addresses immediately when saving (remove last octet)","Ich wurde erwähnt":"I was mentioned","Ihr Anliegen":"Your request","Ihr Name":"Your name","Ihre Bewertung – {}":"Your rating – {}","Ihre E-Mail-Adresse":"Your email address","Ihre Kontaktdaten":"Your contact details","Ihre Nachricht...":"Your message...","Ihre Nachricht…":"Your message…","Im Browser öffnen: zusammenführen, aufteilen, drucken":"Open in browser: merge, split, print","Im Zeitraum wurden noch keine Abfragen verschickt.":"No surveys were sent in this period.","Immer (zum Testen)":"Always (for testing)","Immer an einen bestimmten Mitarbeiter":"Always to a specific staff member","Impressum":"Legal notice","In 1 Stunde":"In 1 hour","In 1 Woche":"In 1 week","In 3 Stunden":"In 3 hours","In 3 Tagen":"In 3 days","In Arbeit":"In progress","In Betrieb":"Operational","In Ordner verschieben":"Move to folder","In der Datei":"In the file","Inaktiv":"Inactive","Individuelle Berechtigungen":"Individual permissions","Individuelle Ticket-Felder":"Custom ticket fields","Informationen":"Information","Inhalt":"Content","Inhalt / Zusammenfassung":"Content / summary","Installierte Sprachen":"Installed languages","Installierte Version":"Installed version","Intern":"Internal","Interne Notiz":"Internal note","Interne Notizen":"Internal notes","Interne Notizen (vertraulich)":"Internal notes (confidential)","Interne Notizen schreiben":"Write internal notes","Interner Chat":"Internal chat","Interner Kommentar…":"Internal comment…","Jahr":"Year","Jeder Job kann hier zusätzlich jederzeit sofort manuell gestartet werden.":"Each job can also be started manually at any time here.","Jedes Postfach wird beim Abruf (Cronjob":"Each mailbox is queried individually when fetching (cron job","Jetzt anmelden":"Sign in now","Jetzt ausführen":"Run now","Jetzt erneut versuchen":"Try again now","Jetzt registrieren":"Register now","Kanal (web/email)":"Channel (web/email)","Kanal-Einstellungen → Integrationen → Webhooks → Neuer Webhook":"Channel settings → Integrations → Webhooks → New webhook","Katalog aktualisieren":"Refresh catalog","Kategorie":"Category","Kategorie wirklich löschen?":"Really delete this category?","Kategorien":"Categories","Kein Hilfethema":"No help topic","Kein Konto nötig:":"No account needed:","Kein Kunde ausgewählt.":"No customer selected.","Kein Live-Chat ausgewählt":"No live chat selected","Kein Logo hinterlegt.":"No logo stored.","Kein Spam":"Not spam","Kein Ticket ausgewählt.":"No ticket selected.","Kein Titelbild hinterlegt.":"No cover image stored.","Kein Token eingetragen (öffentlicher Zugriff).":"No token entered (public access).","Kein Update seit X Stunden":"No update for X hours","Kein Zugriff.":"No access.","Kein eigenes Favicon hinterlegt.":"No custom favicon stored.","Keine":"None","Keine Aktualisierung seit …":"No update since …","Keine Antwort seit …":"No reply since …","Keine Aufgaben gefunden.":"No tasks found.","Keine Aufgaben.":"No tasks.","Keine Berechtigungen":"No permissions","Keine Daten verfügbar.":"No data available.","Keine Einträge.":"No entries.","Keine Erwähnungen vorhanden.":"No mentions.","Keine Kunden gefunden.":"No customers found.","Keine Meldungen.":"No messages.","Keine Mitarbeiter gefunden.":"No staff found.","Keine Mitarbeiter-Antwort seit X Stunden":"No staff reply for X hours","Keine Signatur":"No signature","Keine Signatur hinterlegt.":"No signature stored.","Keine Störungen in den letzten 14 Tagen.":"No incidents in the last 14 days.","Keine Tickets gefunden.":"No tickets found.","Keine Tickets im gewählten Zeitraum.":"No tickets in the selected period.","Keine Tickets mit diesen Filtern. Unter „Aktiv“ kannst du „Alle Status“ wählen, um auch erledigte Tickets zu sehen.":"No tickets with these filters. Under \"Active\" you can choose \"All statuses\" to also see completed tickets.","Keine Tickets vorhanden.":"No tickets available.","Keine Treffer für diese Suche.":"No results for this search.","Keine Treffer.":"No results.","Keine Verbindung zum Server – nächster Versuch in {} s. Bereits geladene Daten sind eventuell veraltet.":"No connection to the server – next attempt in {} s. Data already loaded may be outdated.","Keine Verbindung zum Server.":"No connection to the server.","Keine Verbindung zum Server. Es wird automatisch erneut versucht.":"No connection to the server. It will be retried automatically.","Keine Zeiteinträge im gewählten Zeitraum.":"No time entries in the selected period.","Keine aktive DeskHub-Sitzung.":"No active DeskHub session.","Keine aktiven Chats":"No active chats","Keine lokale Anmeldung vorhanden.":"No local sign-in available.","Keine passende Antwort: Der Assistent würde anbieten, ein Ticket zu erstellen. Lege dafür eine zusätzliche Antwort an oder ergänze die Wissensdatenbank.":"No matching answer: the assistant would offer to create a ticket. Create an additional answer for it or extend the knowledge base.","Keine vorhanden.":"None available.","Keine weitere Einstellung nötig.":"No further setting needed.","Keine weiteren Mitarbeiter vorhanden.":"No more staff available.","Keine weiteren Tickets.":"No more tickets.","Kennzahlen":"Key figures","Klicke rechts auf einen Mitarbeiter":"Click a staff member on the right","Kollaboratoren":"Collaborators","Kollegen auswählen":"Select colleagues","Kommentieren":"Comment","Konfiguration":"Configuration","Kontaktdaten":"Contact details","Kontakte":"Contacts","Kontakte werden geladen …":"Loading contacts …","Konten und Zugriff":"Accounts and access","Konten, Rollen, Abteilungszugriff":"Accounts, roles, department access","Konto":"Account","Konto erstellen":"Create account","Konto erstellen – {}":"Create account – {}","Kontodaten":"Account details","Kontoinformationen":"Account information","Kontotyp":"Account type","Kopieren":"Copy","Kostenloses Konto erstellen":"Create a free account","Kunde":"Customer","Kunde nicht gefunden.":"Customer not found.","Kunde tippt …":"Customer is typing …","Kunden":"Customers","Kunden erinnern, wenn sie auf eine Antwort nicht reagieren (einmalig, vor dem automatischen Schließen)":"Remind customers if they do not respond to a reply (once, before automatic closing)","Kunden per E-Mail benachrichtigen":"Notify customers by email","Kunden werden geladen …":"Loading customers …","Kunden zu Firmen bündeln":"Group customers into companies","Kunden-Organisationen":"Customer organizations","Kundendaten":"Customer data","Kundenerinnerung":"Customer reminder","Kundenerinnerung jetzt manuell ausführen?":"Run customer reminder manually now?","Kundenprofil":"Customer profile","Kundenprofil wird geladen …":"Loading customer profile …","Kundenprofil: {}":"Customer profile: {}","Kundenverzeichnis":"Customer directory","Kundenzufriedenheit":"Customer satisfaction","Kundenzufriedenheit (letzte 30 Tage)":"Customer satisfaction (last 30 days)","Kundenzufriedenheit ({} Tage)":"Customer satisfaction ({} days)","Kurzbeschreibung (optional)":"Short description (optional)","Kurze Zusammenfassung Ihres Problems":"Short summary of your problem","Lade …":"Loading …","Lade…":"Loading…","Leere Vorlage laden":"Load empty template","Legen Sie fest, welche zusätzlichen Bereiche auf der Kundenportal-Startseite angezeigt werden.":"Choose which additional sections are shown on the customer portal home page.","Leiter, Mitglieder, E-Mail":"Lead, members, email","Leiter: {} · {} Mitglieder":"Lead: {} · {} members","Leitung":"Lead","Leserecht":"Read permission","Letzte 30 Tage":"Last 30 days","Letzte 7 Tage":"Last 7 days","Letzte Zustellung":"Last delivery","Letzter Abruf":"Last fetch","Letzter Abruf: {}.":"Last fetch: {}.","Letztes Auto-Backup":"Last auto backup","Letztes Backup":"Last backup","Link anfordern":"Request link","Link kopieren":"Copy link","Link kopiert":"Link copied","Link nicht gültig":"Link not valid","Link-Adresse":"Link address","Live-Chat":"Live chat","Live-Chat (Kunden)":"Live chat (customers)","Live-Chat im Kundenportal (Chat-Widget unten rechts, auch ohne Kundenkonto nutzbar)":"Live chat in the customer portal (chat widget at the bottom right, also usable without a customer account)","Live-Chat ist derzeit":"Live chat is currently","Live-Chat {} · Wissensdatenbank im Portal {}":"Live chat {} · Knowledge base in the portal {}","Log geleert":"Log cleared","Log leeren":"Clear log","Lädt …":"Loading …","Löschen":"Delete","Lösen":"Resolve","Lösung":"Solution","Lösung (Std)":"Resolution (hrs)","Lösung (h)":"Resolution (h)","Lösung fällig":"Resolution due","Lösung fällig: {}":"Resolution due: {}","Lösungszeit (Std)":"Resolution time (hrs)","Lösungszeit (Std.)":"Resolution time (hrs.)","Mail senden nach":"Send email after","Manuell":"Manual","Manuelles Update":"Manual update","Markierte Tickets wirklich unwiderruflich löschen?":"Really delete the selected tickets permanently?","Maximale Dateigröße":"Maximum file size","Mecklenburg-Vorpommern":"Mecklenburg-Western Pomerania","Mehr":"More","Mehr laden":"Load more","Mehrere":"Several","Mehrere Adressen auf einmal anlegen":"Create several addresses at once","Mehrere sperren":"Block several","Mehrzeiliger Text":"Multi-line text","Mein Profil":"My profile","Mein Status":"My status","Mein Status:":"My status:","Meine":"My","Meine Ansicht":"My view","Meine Erwähnungen":"My mentions","Meine Signatur":"My signature","Meine Signatur (nicht hinterlegt)":"My signature (not set)","Meine Signatur bearbeiten":"Edit my signature","Meine Tickets":"My tickets","Meine Tickets ansehen":"View my tickets","Meine Tickets – {}":"My tickets – {}","Meine offenen":"My open","Melden Sie sich an, um alle Ihre Tickets im Überblick zu sehen.":"Sign in to see all your tickets at a glance.","Meldung gelöscht":"Message deleted","Meldung löschen":"Delete message","Meldung löschen?":"Delete message?","Meldung veröffentlicht":"Message published","Meldungen":"Messages","Meldungen (letzte 14 Tage)":"Messages (last 14 days)","Mindestens 10 Zeichen mit Groß- und Kleinbuchstaben, einer Ziffer und einem Sonderzeichen.":"At least 10 characters with upper and lower case letters, a digit and a special character.","Mindestens eine Bedingung":"At least one condition","Minuten":"Minutes","Minuten (5–480). Countdown wird oben rechts angezeigt.":"Minutes (5–480). Countdown is shown at the top right.","Minuten ohne Aktivität":"Minutes without activity","Mir zugewiesen":"Assigned to me","Mir zuweisen":"Assign to me","Mit Windows starten":"Start with Windows","Mit deinem Mitarbeiterkonto anmelden":"Sign in with your staff account","Mit den Apps bearbeitet ihr Tickets am PC oder unterwegs. Beide haben ein helles und dunkles Design.":"With the apps you work on tickets at the PC or on the go. Both have a light and a dark design.","Mitarbeiter":"Staff","Mitarbeiter (nur bei „Immer an einen bestimmten Mitarbeiter“)":"Staff member (only for \"Always to a specific staff member\")","Mitarbeiter auswählen":"Select staff member","Mitarbeiter können diese Textbausteine beim Beantworten eines Tickets per Klick in die Antwort einfügen.":"Staff can insert these text snippets into a reply with one click when answering a ticket.","Mitarbeiter löschen":"Delete staff member","Mitarbeiter tippt …":"Agent is typing …","Mitarbeiter verwalten":"Manage staff","Mitarbeiter werden geladen …":"Loading staff …","Mitarbeiter wirklich löschen?":"Really delete this staff member?","Mitarbeiter zuweisen":"Assign staff member","Mitarbeiter-Anmeldung":"Staff sign-in","Mitarbeiter-Bereich":"Staff area","Mitarbeiter-Gruppen":"Staff groups","Mitarbeiter-Login – {}":"Staff login – {}","Mitarbeiter-Login →":"Staff login →","Mitarbeiter-Verzeichnis":"Staff directory","Mitarbeiterbereich":"Staff area","Mitglied in Teams":"Member of teams","Mitglieder":"Members","Mitglieder (2)":"Members (2)","Mitglieder lassen sich nach dem Erstellen unter „Mitarbeiter“ zuweisen.":"Members can be assigned under \"Staff\" after creating.","Mitglieder: {}":"Members: {}","Mittwoch":"Wednesday","Monat":"Month","Monate":"Months","Monatlich":"Monthly","Montag":"Monday","Morgen 8:00":"Tomorrow 8:00","Morgen früh (8:00 Uhr)":"Tomorrow morning (8:00)","Nach Mitarbeiter:":"By staff member:","Nach Priorität":"By priority","Nach Status":"By status","Nach Stunden":"By hours","Nach dem Update Browser-Cache leeren":"Clear the browser cache after the update","Nach wie vielen Tagen ohne Kundenreaktion":"After how many days without customer response","Nach wie vielen Tagen ohne Reaktion":"After how many days without response","Nachricht":"Message","Nachricht eingeben...":"Enter message...","Nachricht verfassen":"Compose message","Nachricht …":"Message …","Nachricht…":"Message…","Nachricht… (Strg+Enter zum Senden)":"Message… (Ctrl+Enter to send)","Name der Ansicht":"Name of the view","Name der Vorlage":"Name of the template","Name des Assistenten":"Name of the assistant","Name des Kunden":"Customer name","Name in der Benachrichtigung (optional)":"Name in the notification (optional)","Name oder E-Mail":"Name or email","Name oder E-Mail suchen…":"Search name or email…","Nebeneinander":"Side by side","Neu":"New","Neue Abteilung":"New department","Neue Abteilung hinzufügen":"Add new department","Neue Adresse":"New address","Neue Antwort":"New reply","Neue Antwort (an Kunde)":"New reply (to customer)","Neue Aufgabe":"New task","Neue E-Mail-Adresse":"New email address","Neue Eskalationsregel":"New escalation rule","Neue Kategorie":"New category","Neue Kundenantwort":"New customer reply","Neue Liste hinzufügen":"Add new list","Neue Meldung (Störung oder Wartung)":"New message (incident or maintenance)","Neue Organisation":"New organization","Neue Priorität":"New priority","Neue Priorität hinzufügen":"Add new priority","Neue Regel":"New rule","Neue Regel hinzufügen":"Add new rule","Neue Rolle":"New role","Neue Rolle hinzufügen":"Add new role","Neue Seite hinzufügen":"Add new page","Neue Stufe":"New level","Neue Ticket-Regel":"New ticket rule","Neue Tickets automatisch zuweisen":"Assign new tickets automatically","Neue Tickets, Kundenantworten und Zuweisungen":"New tickets, customer replies and assignments","Neue Vorlage":"New template","Neuen Agenten hinzufügen":"Add new agent","Neuen Chat starten":"Start a new chat","Neuen SLA-Plan hinzufügen":"Add new SLA plan","Neuen Schlüssel erstellen":"Create new key","Neuen Webhook hinzufügen":"Add new webhook","Neuer Agent":"New agent","Neuer Artikel":"New article","Neuer Dienst, z. B. Webseite":"New service, e.g. website","Neuer Live-Chat":"New live chat","Neuer Mitarbeiter":"New staff member","Neuer SLA-Plan":"New SLA plan","Neues Bild hochladen (JPG, PNG, WEBP · max. 2 MB)":"Upload new image (JPG, PNG, WEBP · max. 2 MB)","Neues Feld":"New field","Neues Hilfethema":"New help topic","Neues Hilfethema hinzufügen":"Add new help topic","Neues Passwort":"New password","Neues Passwort (leer = unverändert)":"New password (empty = unchanged)","Neues Postfach":"New mailbox","Neues Team":"New team","Neues Team hinzufügen":"Add new team","Neues Ticket":"New ticket","Neues Ticket (an Abteilung)":"New ticket (to department)","Neues Ticket erstellen":"Create new ticket","Neues Ticket eröffnen":"Open a new ticket","Neues Ticket für diesen Kunden":"New ticket for this customer","Neues Ticket – {}":"New ticket – {}","Nicht angemeldet.":"Not signed in.","Nicht automatisch zuweisen":"Do not assign automatically","Nicht für den Produktivbetrieb geeignet. Bitte in":"Not suitable for production use. Please in","Nicht gespeichert: bitte Uhrzeiten prüfen (z. B. 22:00) oder später erneut versuchen.":"Not saved: please check the times (e.g. 22:00) or try again later.","Nicht mehr beobachten":"Stop watching","Nicht prüfbar: die eigene Adresse ließ sich nicht abrufen.":"Cannot be checked: your own address could not be fetched.","Nicht unterstützte HTTP-Methode: {}":"Unsupported HTTP method: {}","Nicht zugew.":"Unassigned","Nicht zugewiesen":"Unassigned","Niedersachsen":"Lower Saxony","Niedrig":"Low","Niemand. Beobachter bekommen Benachrichtigungen, ohne dass ihnen das Ticket zugewiesen ist.":"Nobody. Watchers receive notifications without the ticket being assigned to them.","Niemand. Cc-Empfänger bekommen die öffentlichen Antworten per E-Mail in Kopie.":"Nobody. Cc recipients receive the public replies by email in copy.","Noch kein Konto?":"No account yet?","Noch kein Postfach angelegt.":"No mailbox created yet.","Noch keine API-Schlüssel angelegt.":"No API keys created yet.","Noch keine Abfragen verschickt.":"No surveys sent yet.","Noch keine Abteilungen.":"No departments yet.","Noch keine Artikel.":"No articles yet.","Noch keine Backups vorhanden.":"No backups available yet.","Noch keine Dienste eingetragen.":"No services entered yet.","Noch keine E-Mail-Adressen angelegt.":"No email addresses created yet.","Noch keine Einträge.":"No entries yet.","Noch keine Eskalationsregeln. Erstellen Sie eine neue Regel mit dem Button oben.":"No escalation rules yet. Create a new rule with the button above.","Noch keine Eskalationsregeln. Mit + eine neue Regel anlegen.":"No escalation rules yet. Create a new rule with +.","Noch keine Feiertage eingetragen.":"No holidays entered yet.","Noch keine Hilfethemen.":"No help topics yet.","Noch keine Listen angelegt.":"No lists created yet.","Noch keine Muster angelegt.":"No patterns created yet.","Noch keine Nachrichten vorhanden.":"No messages yet.","Noch keine Nachrichten.":"No messages.","Noch keine Nachrichten. Schreib die erste!":"No messages yet. Write the first one!","Noch keine Notizen.":"No notes yet.","Noch keine Organisationen angelegt.":"No organizations created yet.","Noch keine Organisationen.":"No organizations yet.","Noch keine Plugins installiert.":"No plugins installed yet.","Noch keine Prioritäten.":"No priorities yet.","Noch keine SLA-Pläne angelegt.":"No SLA plans created yet.","Noch keine Seiten angelegt.":"No pages created yet.","Noch keine Teams angelegt.":"No teams created yet.","Noch keine Teams.":"No teams yet.","Noch keine Ticket-Regeln angelegt.":"No ticket rules created yet.","Noch keine Ticket-Regeln. Mit + eine neue Regel anlegen.":"No ticket rules yet. Create a new rule with +.","Noch keine Vorlagen.":"No templates yet.","Noch keine Webhooks angelegt.":"No webhooks created yet.","Noch keine Zeit erfasst.":"No time recorded yet.","Noch keine benutzerdefinierten Rollen angelegt.":"No custom roles created yet.","Noch keine benutzerdefinierten Rollen. Ohne eigene Rolle gelten die Standard-Agentenrechte.":"No custom roles yet. Without a role of their own, the default agent permissions apply.","Noch keine eigenen Themes hochgeladen.":"No custom themes uploaded yet.","Noch keine gespeicherten Ansichten vorhanden.":"No saved views yet.","Noch keine individuellen Felder angelegt.":"No custom fields created yet.","Noch keine zusätzlichen Antworten.":"No additional answers yet.","Noch nie":"Never","Nordrhein-Westfalen":"North Rhine-Westphalia","Normale Tickets":"Normal tickets","Notiz":"Note","Notiz (optional)":"Note (optional)","Notiz (optional, z.B. «In Besprechung bis 14:00»)":"Note (optional, e.g. \"In a meeting until 14:00\")","Notiz gespeichert":"Note saved","Notiz hinzufügen…":"Add note…","Notiz speichern":"Save note","Notizen":"Notes","Notizen (optional)":"Notes (optional)","Nr.":"No.","Nummer kopieren":"Copy number","Nummer kopiert":"Number copied","Nur Abteilung":"Department only","Nur Administratoren dürfen die Statusseite ändern.":"Only administrators may change the status page.","Nur Bereiche, für die du freigeschaltet bist.":"Only areas you are authorized for.","Nur Lesezugriff, solange {} diesen Chat bearbeitet.":"Read-only while {} is handling this chat.","Nur Priorität":"Priority only","Nur Status (kommagetrennt, z.B. open,pending)":"Only status (comma-separated, e.g. open,pending)","Nur ausfüllen, wenn der Anbieter eine Prüfsumme nennt":"Only fill in if the provider states a checksum","Nur ausfüllen, wenn der Anbieter eine Signatur nennt":"Only fill in if the provider states a signature","Nur benachrichtigen":"Notify only","Nur in der installierten Version verfügbar.":"Only available in the installed version.","Nur interne Notiz speichern":"Save internal note only","Nur nötig, wenn das Repository mit den Releases":"Only needed if the repository with the releases","Nur signierte Releases:":"Signed releases only:","Nur wenn der Live-Chat aus oder niemand erreichbar ist (Standard)":"Only if live chat is off or nobody can be reached (default)","Nur wenn der Live-Chat ausgeschaltet oder niemand erreichbar ist (Standard)":"Only if live chat is switched off or nobody can be reached (default)","Offen":"Open","Offene Tickets":"Open tickets","Offline (Wartungsmodus)":"Offline (maintenance mode)","Offline-Nachricht":"Offline message","Ohne Konto: Geben Sie Ihre Ticketnummer und E-Mail-Adresse ein.":"Without an account: enter your ticket number and email address.","Ohne eigenen Server wird der globale SMTP aus den Einstellungen genutzt.":"Without its own server the global SMTP from the settings is used.","Online Mitarbeiter":"Staff online","Option 1: Datei in den DeskHub-Ordner legen (für große Dateien empfohlen)":"Option 1: put the file in the DeskHub folder (recommended for large files)","Option 2: Direkt hochladen (in Teilen von 5 MB, passend zum Limit dieses Servers)":"Option 2: upload directly (in 5 MB parts, matching this server's limit)","Optionale Felder beim Anlegen:":"Optional fields when creating:","Optionen":"Options","Optionen (eine pro Zeile)":"Options (one per line)","Optionen bei neuem Ticket":"Options for new tickets","Ordner":"Folder","Ordner mit diesen Dateien packen:":"Pack the folder with these files:","Organisation":"Organization","Organisation bearbeiten":"Edit organization","Organisationen":"Organizations","Organisationen verwalten":"Manage organizations","Organisationen werden geladen …":"Loading organizations …","Passwort":"Password","Passwort (leer = unverändert)":"Password (empty = unchanged)","Passwort *":"Password *","Passwort eingeben":"Enter password","Passwort vergessen":"Forgot password","Passwort vergessen – {}":"Forgot password – {}","Passwort vergessen?":"Forgot your password?","Passwort wiederholen":"Repeat password","Passwort ändern":"Change password","Pause":"Break","Pausiert":"Paused","Pflichtfeld":"Required field","Pflichtfelder sind mit":"Required fields are marked with","Platzhalter:":"Placeholders:","Plugins (Hintergrundaufgaben)":"Plugins (background tasks)","Plugins (Hintergrundaufgaben) jetzt manuell ausführen?":"Run plugins (background tasks) manually now?","Popups und stille Zeiten":"Popups and quiet hours","Postfach":"Mailbox","Postfach / Ordner":"Mailbox / folder","Postfach testen":"Test mailbox","Postfächer (E-Mail-Abruf)":"Mailboxes (email fetching)","Postfächer für den E-Mail-Abruf":"Mailboxes for email fetching","Postfächer verwalten →":"Manage mailboxes →","Postfächer werden jetzt direkt bei der jeweiligen":"Mailboxes are now fetched directly at the respective","Priorisiert: mir zugewiesen oder unbearbeitet":"Prioritized: assigned to me or unprocessed","Priorität":"Priority","Priorität bearbeiten":"Edit priority","Priorität für Tickets des Assistenten":"Priority for assistant tickets","Priorität setzen":"Set priority","Priorität wirklich löschen?":"Really delete this priority?","Priorität:":"Priority:","Prioritäten & SLA-Zeiten":"Priorities & SLA times","Prioritäten / SLA":"Priorities / SLA","Prioritäten / SLA verwalten":"Manage priorities / SLA","Prioritäten werden geladen …":"Loading priorities …","Profil":"Profile","Profilbild":"Profile picture","Protokoll wird geladen …":"Loading log …","Prüfen ·":"Check ·","Push fehlgeschlagen:":"Push failed:","Push-Benachrichtigungen senden":"Send push notifications","Push-Benachrichtigungen sind auf diesem Server nicht verfügbar (OpenSSL-Erweiterung fehlt oder PHP zu alt).":"Push notifications are not available on this server (OpenSSL extension missing or PHP too old).","Quelle":"Source","Quelltext":"Source code","Radio-Auswahl (Punkte)":"Radio selection (dots)","Reaktion":"Response","Reaktion (Std)":"Response (hrs)","Reaktion (h)":"Response (h)","Reaktion {} Std · Lösung {} Std":"Response {} hrs · resolution {} hrs","Reaktion: {}h · Lösung: {}h":"Response: {}h · resolution: {}h","Reaktions- und Lösungszeit ab Ticketerstellung bzw. Prioritätsänderung. Mit aktivierten":"Response and resolution time from ticket creation or priority change. With enabled","Reaktions- und Lösungszeiten":"Response and resolution times","Reaktionszeit (Std)":"Response time (hrs)","Reaktionszeit (Std.)":"Response time (hrs.)","Rechnungsfrage":"Billing question","Rechte":"Permissions","Regel":"Rule","Regel bearbeiten":"Edit rule","Regeln für überfällige Tickets":"Rules for overdue tickets","Regeln werden geladen …":"Loading rules …","Registrierte Kundenkonten und Gast-Anfrager (ohne Konto) aus allen Tickets.":"Registered customer accounts and guest requesters (without account) from all tickets.","Registriertes Konto":"Registered account","Reihenfolge":"Order","Reihum an die Mitarbeiter der Abteilung":"Round robin among the department's staff","Reiner Text. Adressen mit https:// werden anklickbar.":"Plain text. Addresses with https:// become clickable.","Rheinland-Pfalz":"Rhineland-Palatinate","Richtung:":"Direction:","Rolle":"Role","Rolle (für Mitarbeiter)":"Role (for staff)","Rolle bearbeiten":"Edit role","Rolle:":"Role:","Rollen":"Roles","Rollen legst du unter":"You create roles under","Rollen und ihre Rechte legst du unter „Rollen“ an.":"You create roles and their permissions under \"Roles\".","Rollen verwalten":"Manage roles","Rollen werden geladen …":"Loading roles …","Rollenname":"Role name","SLA Erste Antwort":"SLA first response","SLA Lösung":"SLA resolution","SLA überschritten":"SLA breached","SLA-Fristen":"SLA deadlines","SLA-Plan":"SLA plan","SLA-Plan (leer = Abteilung/Priorität)":"SLA plan (empty = department/priority)","SLA-Plan (leer = Zeiten der Priorität)":"SLA plan (empty = priority's times)","SLA-Plan aktualisieren":"Update SLA plan","SLA-Pläne":"SLA plans","SLA-Pläne werden geladen …":"Loading SLA plans …","SLA-Prüfung":"SLA check","SLA-Prüfung jetzt manuell ausführen?":"Run SLA check manually now?","SLA-Quote":"SLA rate","SLA-Status":"SLA status","SLA-Uhr nur innerhalb der Geschäftszeiten laufen lassen":"Run the SLA clock only within business hours","SLA-Zeitfenster, Feiertage":"SLA time windows, holidays","SLA: Alle":"SLA: All","SMTP testen":"Test SMTP","SMTP-Server":"SMTP server","SMTP-Versand":"SMTP sending","SQL-Datei hochladen oder Pfad angeben":"Upload SQL file or enter path","SQLite-Modus aktiv:":"SQLite mode active:","Sachsen":"Saxony","Sachsen-Anhalt":"Saxony-Anhalt","Sammelmodus für Übersetzer:":"Collection mode for translators:","Samstag":"Saturday","Schichtplan bearbeiten":"Edit shift schedule","Schließen":"Close","Schließt Tickets, auf die der Kunde nach einer Antwort nicht mehr reagiert.":"Closes tickets the customer no longer responds to after a reply.","Schlüssel":"Key","Schlüssel erstellen":"Create key","Schnellfilter":"Quick filters","Schnellsuche / Befehle":"Quick search / commands","Seit":"Since","Seit 24.10.2026 19:44 geöffnet – wird beim Antworten automatisch als Zeit erfasst.":"Open since 24.10.2026 19:44 – time is recorded automatically when you reply.","Seite nicht gefunden":"Page not found","Seite nicht gefunden – {}":"Page not found – {}","Seite {}":"Page {}","Seite {} · {} Tickets":"Page {} · {} tickets","Seiten":"Pages","Seiten-Verwaltung":"Page management","Seitenleiste einblenden":"Show sidebar","Seitenleiste einklappen":"Collapse sidebar","Senden":"Send","Sendet …":"Sending …","Server-Adresse":"Server address","Server-Fehler ({}).":"Server error ({}).","Server: 0,00 MB":"Server: 0.00 MB","Serveradresse für die Anmeldung":"Server address for sign-in","Serverfehler ({}).":"Server error ({}).","Servicezeit (Std)":"Service time (hrs)","Setzen":"Set","Setzen auf:":"Set to:","Sicherheitsprüfung":"Security check","Signatur":"Signature","Signatur (E-Mail-Fuß für Antworten ohne Mitarbeiter-Signatur)":"Signature (email footer for replies without a staff signature)","Signatur (E-Mail-Fuß für eigene Antworten)":"Signature (email footer for own replies)","Signatur (optional)":"Signature (optional)","Signatur (optional, Base64)":"Signature (optional, Base64)","Sitzung abgelaufen – bitte neu anmelden.":"Session expired – please sign in again.","So kommen Benachrichtigungen auch bei geschlossenem Fenster an. Beenden über das Tray-Menü.":"This way notifications also arrive when the window is closed. Quit via the tray menu.","So nutzt du die API":"How to use the API","Sonntag":"Sunday","Sortieren nach":"Sort by","Sortiermodus:":"Sort mode:","Sortierung":"Sorting","Spam-/Junk-Ordner":"Spam/junk folder","Speichern":"Save","Speichert die aktuellen Filter (Reiter, Suche, Abteilung, Priorität).":"Saves the current filters (tab, search, department, priority).","Speichert die aktuellen Filter.":"Saves the current filters.","Speichert …":"Saving …","Sperre aufgehoben":"Lock released","Sperre aufheben":"Remove lock","Sperre aufheben und übernehmen":"Release lock and take over","Sperren":"Block","Sperrliste":"Block list","Sperrliste ist leer.":"The block list is empty.","Sperrliste ist leer. Mit + eine E-Mail-Adresse oder Domain sperren.":"The block list is empty. Block an email address or domain with +.","Sperrliste wird geladen …":"Loading block list …","Sprachen":"Languages","Stand":"As of","Stand: {} Uhr":"As of: {}","Standard":"Default","Standard-Absenderadresse der Abteilung":"Department's default sender address","Standard-Absenderadresse dieser Abteilung":"Default sender address of this department","Standard-Berichte":"Default reports","Standard-Mitarbeiterrechte (ohne eigene Rolle)":"Default staff permissions (without own role)","Standardabteilung":"Default department","Standardabteilung (bzw. Abteilung des Themas)":"Default department (or the topic's department)","Standardabteilung (für E-Mail-Tickets ohne Zuordnung)":"Default department (for email tickets without assignment)","Standardabteilung verwenden":"Use default department","Standardpriorität (optional)":"Default priority (optional)","Standardsprache (für alle, die noch keine Flagge gewählt haben)":"Default language (for everyone who has not yet chosen a flag)","Startet DeskHub beim Anmelden im Hintergrund (Tray).":"Starts DeskHub in the background (tray) when signing in.","Startseiten-Inhalte":"Home page content","Statistiken":"Statistics","Statistiken werden geladen …":"Loading statistics …","Status & Schichten":"Status & shifts","Status & Vorschau":"Status & preview","Status abfragen:":"Query status:","Status der Mitarbeiter":"Staff status","Status der Mitarbeiter ändern (Admin)":"Change staff status (admin)","Status gesetzt":"Status set","Status geändert":"Status changed","Status nach Antwort":"Status after reply","Status nach dem Senden":"Status after sending","Status prüfen":"Check status","Status setzen":"Set status","Status setzen…":"Set status…","Status von":"Status of","Status von {}":"Status of {}","Status wird geladen …":"Loading status …","Status ändern, z. B. auf Offline setzen":"Change status, e.g. set to offline","Status, Leiter, Mitglieder, E-Mail":"Status, lead, members, email","Status: automatisch":"Status: automatic","Statusseite":"Status page","Statusseite ausgeschaltet":"Status page switched off","Statusseite eingeschaltet":"Status page switched on","Statusseite für Kunden anzeigen":"Show status page to customers","Statusseite für Kunden anzeigen (Link „Status“ im Kundenbereich)":"Show status page to customers (link \"Status\" in the customer area)","Statusseite wird geladen …":"Loading status page …","Std":"hrs","Std.":"hrs.","Stichwörter":"Keywords","Stichwörter (durch Komma)":"Keywords (comma-separated)","Stichwörter (optional, durch Komma getrennt)":"Keywords (optional, comma-separated)","Stichwörter: {}":"Keywords: {}","Stille Zeiten (keine Popups)":"Quiet hours (no popups)","Strg+F5":"Ctrl+F5","Stufe":"Level","Stunden":"Hours","Störung:":"Incident:","Stündliche Prüfung. Letzte Ausführung: noch nie":"Hourly check. Last run: never","Suchbegriff eingeben …":"Enter a search term …","Suche":"Search","Suchen":"Search","Suchen (Betreff, Nummer, Kunde, Text)… Strg+F":"Search (subject, number, customer, text)… Ctrl+F","Suchen …":"Searching …","Suchen…":"Search…","Supportcenter":"Support center","Supportcenter-Startseite":"Support center home","Supportcenter-Startseite – {}":"Support center home – {}","System-Update":"System update","Systemstandard":"System default","Systemstatus":"System status","Systemstatus – {}":"System status – {}","Tag hinzufügen":"Add tag","Tag-Name":"Tag name","Tage":"Days","Tage (am besten kürzer als die Frist zum automatischen Schließen)":"Days (preferably shorter than the period for automatic closing)","Tage ohne Reaktion":"Days without response","Tagen schließen":"days","Tastenkürzel":"Keyboard shortcuts","Team (als Administrator kannst du den Status ändern)":"Team (as administrator you can change the status)","Team bearbeiten":"Edit team","Team-Chat":"Team chat","Team-Übersicht":"Team overview","Team-Übersicht (zum Ändern antippen)":"Team overview (tap to change)","Teamleiter":"Team lead","Teamname":"Team name","Teams verwalten":"Manage teams","Teams werden geladen …":"Loading teams …","Technik":"Technology","Technisches Problem":"Technical issue","Teilweise eingeschränkt":"Partially degraded","Telefon":"Phone","Telefonnummer (optional)":"Phone number (optional)","Telegram Bot-Token":"Telegram bot token","Telegram Chat-ID":"Telegram chat ID","Test-Benachrichtigung":"Test notification","Test-Benachrichtigung – so sieht eine Meldung aus.":"Test notification – this is what a message looks like.","Test-Mail senden":"Send test email","Testen":"Test","Testnachricht an meine Geräte":"Test message to my devices","Testnachricht senden":"Send test message","Tests verwenden die gespeicherten Werte.":"Tests use the saved values.","Text (HTML erlaubt)":"Text (HTML allowed)","Text (Vorschau)":"Text (preview)","Text, wenn er keine Antwort findet":"Text if it finds no answer","Textbaustein wirklich löschen?":"Really delete this text snippet?","Textbausteine verwalten":"Manage text snippets","Textbaustein…":"Text snippet…","Texte der automatischen E-Mails":"Texts of the automatic emails","Texte der automatischen Mails":"Texts of the automatic emails","Thema":"Topic","Thema, Abteilung, Priorität":"Topic, department, priority","Thema, Typ, Abteilung, Priorität":"Topic, type, department, priority","Theme hochladen":"Upload theme","Theme-Paket":"Theme package","Themen":"Topics","Themes aus dem Katalog":"Themes from the catalog","Thüringen":"Thuringia","Ticket #{}":"Ticket #{}","Ticket #{} – {}":"Ticket #{1} – {2}","Ticket anlegen (JSON). Antwort:":"Create ticket (JSON). Response:","Ticket ansehen":"View ticket","Ticket ansehen – {}":"View ticket – {}","Ticket auf später legen":"Snooze ticket","Ticket eingegangen":"Ticket received","Ticket erstellen":"Create ticket","Ticket erstellt":"Ticket created","Ticket erstellt (an Kunde)":"Ticket created (to customer)","Ticket hinzufügen":"Add ticket","Ticket im Namen eines Kunden anlegen – z.B. bei Telefonanrufen oder Vor-Ort-Anfragen.":"Create a ticket on behalf of a customer – e.g. for phone calls or on-site requests.","Ticket inkl. Verlauf unwiderruflich löschen?":"Permanently delete the ticket including its history?","Ticket ist überfällig (SLA überschritten)":"Ticket is overdue (SLA exceeded)","Ticket mir zugewiesen":"Ticket assigned to me","Ticket offen seit X Stunden":"Ticket open for X hours","Ticket schließen (wird angelegt, aber sofort geschlossen)":"Close ticket (is created but closed immediately)","Ticket verknüpfen (Ticket-Nr., optional)":"Link ticket (ticket no., optional)","Ticket verwerfen (z.B. Spam)":"Discard ticket (e.g. spam)","Ticket wird geladen …":"Loading ticket …","Ticket wurde aktualisiert":"Ticket was updated","Ticket wurde {}, aber nicht alle Original-Mails konnten verschoben werden:":"Ticket was {}, but not all original emails could be moved:","Ticket überfällig (an Admins)":"Ticket overdue (to admins)","Ticket(s) anonymisiert (geschlossen/gelöst, älter als 24 Monate).":"ticket(s) anonymized (closed/resolved, older than 24 months).","Ticket, Kunde oder Bereich suchen…":"Search ticket, customer or area…","Ticket-Berechtigungen":"Ticket permissions","Ticket-Bewertung aktivieren (Kunden können nach Schließung 1–5 Sterne vergeben)":"Enable ticket rating (customers can give 1–5 stars after closing)","Ticket-Details":"Ticket details","Ticket-Felder":"Ticket fields","Ticket-Formular":"Ticket form","Ticket-Liste":"Ticket list","Ticket-Nachrichten":"Ticket messages","Ticket-Nr.:":"Ticket no.:","Ticket-Nummer, Betreff, Status":"Ticket number, subject, status","Ticket-Regel bearbeiten":"Edit ticket rule","Ticket-Regeln":"Ticket rules","Ticket-Regeln (Filter)":"Ticket rules (filter)","Ticket-Regeln, Auto-Antworten":"Ticket rules, auto-replies","Ticket-Vorlagen":"Ticket templates","Ticketaktivität":"Ticket activity","Ticketnummer":"Ticket number","Ticketnummer kopieren":"Copy ticket number","Ticketnummer oder Betreff suchen…":"Search ticket number or subject…","Ticketnummer verknüpfen (optional)":"Link ticket number (optional)","Ticketnummer, Betreff, Kunde oder Bereich…":"Ticket number, subject, customer or area…","Tickets ({#})":"Tickets ({})","Tickets automatisch schließen, wenn der Kunde nach einer Antwort nicht mehr reagiert":"Close tickets automatically if the customer does not respond after a reply","Tickets beantworten":"Reply to tickets","Tickets durchsuchen":"Search tickets","Tickets erstellen":"Create tickets","Tickets gesamt":"Total tickets","Tickets im Namen von Kunden erstellen":"Create tickets on behalf of customers","Tickets löschen":"Delete tickets","Tickets lösen/schließen":"Resolve/close tickets","Tickets schließen/lösen":"Close/resolve tickets","Tickets verschieben":"Move tickets","Tickets werden geladen …":"Loading tickets …","Tickets zusammenführen":"Merge tickets","Tickets zuweisen":"Assign tickets","Tickets übertragen":"Transfer tickets","Ticketstatus überprüfen":"Check ticket status","Titel":"Title","Titelbild (Hero-Banner)":"Cover image (hero banner)","To-dos für dich und dein Team":"To-dos for you and your team","Token aus Gitea einfügen":"Paste token from Gitea","Typ":"Type","Täglich":"Daily","Um Ihnen weiterhelfen zu können, benötigen wir noch ein paar zusätzli…":"To help you further, we need a few additional details…","Unbeantwortet":"Unanswered","Unbekannt":"Unknown","Ungültige Eingabe.":"Invalid input.","Ungültige verschlüsselte Sitzungsdaten.":"Invalid encrypted session data.","Untereinander":"Stacked","Unterhaltung wird geladen …":"Loading conversation …","Unterstützt werden SQL-Dumps von osTicket 1.10 bis 1.18 (phpMyAdmin-Export oder":"SQL dumps from osTicket 1.10 to 1.18 are supported (phpMyAdmin export or","Update für die Kunden":"Update for the customers","Update senden":"Send update","Update veröffentlicht":"Update published","Updates: neue Datei laden und darüber installieren. Eure Anmeldung bleibt erhalten.":"Updates: load a new file and install over it. Your sign-in is kept.","Ursache gefunden":"Cause found","Verbindung testen":"Test connection","Verbindung wiederhergestellt":"Connection restored","Verfügbar":"Available","Verfügbare Platzhalter:":"Available placeholders:","Verfügbarkeit":"Availability","Verfügbarkeit & Schichten":"Availability & shifts","Verfügbarkeit und Team-Übersicht":"Availability and team overview","Verfügbarkeit wird geprüft…":"Checking availability…","Vergangene Meldungen (letzte 14 Tage)":"Past incidents (last 14 days)","Verknüpftes Ticket":"Linked ticket","Verknüpfung":"Link","Verlauf":"History","Verlauf (1 Nachricht)":"History (1 message)","Verlauf und Status Ihrer aktuellen und bisherigen Supportanfragen.":"History and status of your current and previous support requests.","Verlauf, Tags und Collaborators werden ins Ziel-Ticket verschoben. Dieses Ticket wird geschlossen.":"History, tags and collaborators are moved to the target ticket. This ticket will be closed.","Verlinkter Teil des Textes":"Linked part of the text","Verschlüsselung":"Encryption","Vertraulich/Intern":"Confidential/Internal","Verwalten":"Manage","Verwaltung":"Administration","Verwenden →":"Use →","Verzeichnis":"Directory","Veröffentlichen":"Publish","Veröffentlicht":"Published","Vielen Dank für Ihre Nachricht. Wir haben Ihr Anliegen erhalten und k…":"Thank you for your message. We have received your request and…","Vielen Dank! Ihr Ticket wurde erstellt.":"Thank you! Your ticket has been created.","Von":"From","Von (HH:MM)":"From (HH:MM)","Von (JJJJ-MM-TT)":"From (YYYY-MM-DD)","Vorgabe für den Betreff":"Default for the subject","Vorhandene Backups":"Existing backups","Vorhandene Vorlagen":"Existing templates","Vorlage":"Template","Vorlage einfügen":"Insert template","Vorlage laden":"Load template","Vorlagen":"Templates","Vorlagen / Textbausteine":"Templates / text snippets","Vorlagen werden geladen …":"Loading templates …","Vorname Nachname":"First name last name","Vorschau":"Preview","Wann ist er aktiv?":"When is it active?","Wann?":"When?","Wartend":"Waiting","Wartet auf Kunde":"Waiting for customer","Wartung":"Maintenance","Was ist passiert? Was tun wir dagegen?":"What happened? What are we doing about it?","Was ist zu tun?":"What needs to be done?","Was wird anonymisiert?":"What is anonymized?","Website-URL":"Website URL","Weiter ›":"Next ›","Weitere":"More","Weitere Aktionen":"More actions","Weitere Informationen benötigt":"More information needed","Weitere Informationen → Trotzdem ausführen":"More information → Run anyway","Weitere Tickets des Kunden":"More tickets from the customer","Weitere laden":"Load more","Weiterer Treffer:":"Another match:","Weiterer Treffer: {}":"Another match: {}","Wenige Stunden pro Tag (z. B. nur abends) lassen die Fälligkeit in":"A few hours per day (e.g. only evenings) let the due date in","Wenn …":"If …","Wert":"Value","Wie System":"Like system","Wie Windows (automatisch)":"Like Windows (automatic)","Wie erstelle ich ein Ticket?":"How do I create a ticket?","Wie können wir helfen?":"How can we help?","Wie lange dauert die Bearbeitung?":"How long does processing take?","Wieder geöffnet":"Reopened","Wieder öffnen":"Reopen","Wiederkehrende Tickets":"Recurring tickets","Wiederkehrende Tickets jetzt manuell ausführen?":"Run recurring tickets manually now?","Wiedervorlage":"Snooze","Wiedervorlage aufgehoben":"Snooze removed","Wiedervorlage aufheben":"Remove snooze","Wiedervorlage fällig":"Snooze due","Wiedervorlage gesetzt":"Snooze set","Wiedervorlage und Beobachten":"Snooze and watching","Wiedervorlagen":"Snoozes","Wiedervorlagen jetzt manuell ausführen?":"Run snoozes manually now?","Willkommen zurück, {}":"Welcome back, {}","Willkommen, Gast":"Welcome, Guest","Willkommen, {}":"Welcome, {}","Windows-Benachrichtigungen anzeigen":"Show Windows notifications","Wir senden Ihnen einen Link zum Zurücksetzen.":"We will send you a reset link.","Wird beobachtet":"Being monitored","Wird berechnet …":"Calculating …","Wird erstellt …":"Creating …","Wird geladen …":"Loading …","Wird gerade von {} bearbeitet – Sie sehen live mit, können hier aber nicht antworten.":"Currently being handled by {} – you can watch live but cannot reply here.","Wird gerade von {} bearbeitet.":"Currently being handled by {}.","Wird gespeichert …":"Saving …","Wird gesperrt …":"Blocking …","Wird im Browser-Tab, in E-Mails und in der Web-App angezeigt.":"Shown in the browser tab, in emails and in the web app.","Wird in E-Mail-Links verwendet. Kein abschließendes /":"Used in email links. No trailing /","Wird nur an öffentliche Antworten angehängt, nicht an interne Notizen.":"Only appended to public replies, not to internal notes.","Wird untersucht":"Investigating","Wird verwendet, wenn ein eingegangenes E-Mail-Ticket als Spam markiert wird.":"Used when an incoming email ticket is marked as spam.","Wirklich löschen?":"Really delete?","Wirklich sperren?":"Really block?","Wirksame Rechte":"Effective permissions","Wissensdatenbank":"Knowledge base","Wissensdatenbank durchsuchen oder direkt ein Ticket eröffnen – wir antworten so schnell wie möglich.":"Search the knowledge base or open a ticket right away – we will reply as quickly as possible.","Wissensdatenbank im Kundenportal anzeigen":"Show knowledge base in the customer portal","Wissensdatenbank verwalten":"Manage knowledge base","Wissensdatenbank wird geladen …":"Loading knowledge base …","Wissensdatenbank – {}":"Knowledge base – {}","Woche":"Week","Wochenzeiten":"Weekly hours","Wofür soll die App ein Popup zeigen?":"What should the app show a popup for?","Wonach suchen Sie?":"What are you looking for?","Worum geht es?":"What is it about?","Wähle die Abteilungen, zu denen der Agent Zugang haben soll.":"Choose the departments the agent should have access to.","Wähle, wofür die App ein Popup zeigen soll.":"Choose what the app should show a popup for.","Wöchentlich":"Weekly","Wörter":"Words","Wörter, die Kunden und Artikel unterschiedlich nennen, aber dasselbe meinen. Eine Gruppe je Zeile, durch Komma getrennt, zum Beispiel „Zugang, Einladung, beitreten“. Einige Gruppen sind eingebaut, deine ergänzen oder überschreiben sie.":"Words that customers and articles name differently but mean the same. One group per line, separated by commas, for example \"access, invitation, join\". Some groups are built in, yours add to or override them.","ZIP-Datei":"ZIP file","ZIP-Datei von Gitea hochladen und installieren.":"Upload and install a ZIP file from Gitea.","Zahl":"Number","Zeigt, was Kunden den Assistenten gefragt haben und ob er eine Antwort fand. Gespeichert werden nur Frage und Ergebnis (kein Name, keine E-Mail-Adresse).":"Shows what customers asked the assistant and whether it found an answer. Only the question and result are stored (no name, no email address).","Zeigt, was Kunden gefragt haben und ob der Assistent eine Antwort fand. Gespeichert werden nur Frage und Ergebnis.":"Shows what customers asked and whether the assistant found an answer. Only the question and result are stored.","Zeigt, was der Assistent auf eine Kundenfrage antworten würde (ohne Zähler und ohne Ticket).":"Shows what the assistant would answer to a customer question (without counters and without a ticket).","Zeit":"Time","Zeiten der Priorität verwenden":"Use the priority's times","Zeiten je Abteilung oder Thema":"Times per department or topic","Zeiten je Mitarbeiter":"Times per staff member","Zeiten werden geladen …":"Loading times …","Zeiterfassung":"Time tracking","Zeiterfassung einsehen":"View time tracking","Zeitplan aus":"Schedule off","Zeitplan speichern":"Save schedule","Zeitraum":"Period","Zeitzone":"Time zone","Ziel":"Target","Zielordner":"Target folder","Zielordner (bei Verschieben)":"Target folder (when moving)","Zielverzeichnis":"Target directory","Zitieren":"Quote","Zu einem Zeitpunkt …":"At a point in time …","Zu lange offen seit …":"Open for too long since …","Zufriedenheitsabfrage":"Satisfaction survey","Zufriedenheitsabfrage jetzt manuell ausführen?":"Run the satisfaction survey manually now?","Zugang":"Access","Zugang zum Update-Server":"Access to the update server","Zugang, Einladung, beitreten Geld zurück, Erstattung, Rückzahlung":"access, invitation, join money back, refund, reimbursement","Zugewiesen":"Assigned","Zugewiesen an":"Assigned to","Zugewiesen:":"Assigned:","Zugewiesenen Mitarbeiter benachrichtigen":"Notify assigned staff member","Zugriffstoken":"Access token","Zuletzt aktiv":"Last active","Zuletzt aktualisiert":"Last updated","Zuletzt genutzt":"Last used","Zum Ändern fehlt das Recht „Teams verwalten“.":"The \"Manage teams\" permission is missing to change this.","Zuordnung":"Assignment","Zurück":"Back","Zurück zu Tickets":"Back to tickets","Zurück zur Startseite":"Back to home","Zurück zur Ticketliste":"Back to ticket list","Zurückgesetzt":"Reset","Zurückgestellt":"Postponed","Zurücksetzen":"Reset","Zusammenführen":"Merge","Zustand":"State","Zustand geändert":"State changed","Zustand jetzt:":"Current state:","Zustand jetzt: {}":"State now: {}","Zuständig":"Responsible","Zuständigen benachrichtigen":"Notify responsible person","Zusätzlich":"Additionally","Zusätzliche Antwort":"Additional answer","Zusätzliche Antworten":"Additional answers","Zuweisen":"Assign","Zuweisen an":"Assign to","Zuweisen…":"Assign…","Zuweisung":"Assignment","Zuweisung geändert":"Assignment changed","Zuweisungen änderst du unter":"You change assignments under","Zuweisungen änderst du unter „Mitarbeiter“.":"You change assignments under \"Staff\".","Zweiten Faktor bestätigen":"Confirm second factor","absteigend":"descending","aktiv (Kunden sehen den Assistenten)":"active (customers see the assistant)","aktiv gerade eben":"active just now","alle 2 Minuten":"every 2 minutes","alle 30 Sekunden":"every 30 seconds","alle 5 Minuten":"every 5 minutes","als Spam markiert":"marked as spam","an":"on","auf":"to","ausgeschaltet":"switched off","beginnt mit":"starts with","bis":"until","bis (HH:MM)":"until (HH:MM)","ein Ticket eröffnen":"open a ticket","eingeschaltet":"switched on","endet mit":"ends with","enthält":"contains","enthält nicht":"does not contain","gekennzeichnet.":"marked.","geändert":"changed","in Ordnung":"OK","in {#} Min":"in {} min","in {#} Std {#} Min":"in {1}h {2}m","ist genau":"is exactly","jede Minute":"every minute","kein bestimmter Dienst":"no specific service","keine Abteilung":"no department","keine Abteilungs-E-Mail":"no department email","leer = von überall; sonst kommagetrennt":"empty = from anywhere; otherwise comma-separated","mind. 10 Zeichen, Groß-/Klein, Ziffer, Sonderzeichen":"min. 10 characters, upper/lower case, digit, special character","mitarbeiter@firma.de":"staff@company.com","nicht vom Herausgeber geprüft":"not verified by the publisher","nicht ändern":"do not change","noch keins":"none yet","oder":"or","oder direkt":"or directly","privat":"private","um alle Ihre Tickets im Überblick zu behalten.":"to keep all your tickets at a glance.","und":"and","von":"from","von (HH:MM)":"from (HH:MM)","vor {#} Min (überfällig)":"{} min ago (overdue)","vor {#} Min.":"{} min. ago","vor {#} Std {#} Min (überfällig)":"{1}h {2}m ago (overdue)","wird nur an Antworten an den Kunden angehängt":"is only attached to replies to the customer","z. B. E-Mail-Versand verzögert":"e.g. email delivery delayed","z. B. Ich habe die Nutzerbedingungen gelesen und akzeptiert.":"e.g. I have read and accept the terms of use.","z. B. Lieferzeit, Paket, Lieferung":"e.g. delivery time, parcel, delivery","z. B. Meine überfälligen":"e.g. My overdue","z. B. Mit freundlichen Grüßen Ihr Support-Team":"e.g. Kind regards, your support team","z. B. Mit freundlichen Grüßen Max Mustermann":"e.g. Kind regards, Max Mustermann","z. B. Monatsbericht Support":"e.g. Monthly support report","z. B. Neuer Mitarbeiter":"e.g. New employee","z. B. Nutzerbedingungen":"e.g. Terms of use","z. B. Passwort zurücksetzen, Rechnung, Versand …":"e.g. reset password, invoice, shipping …","z. B. Vertragsnummer":"e.g. Contract number","z. B. Website-Kontaktformular":"e.g. Website contact form","z. B. Wie lange dauert der Versand?":"e.g. How long does shipping take?","z. B. Wie setze ich mein Passwort zurück?":"e.g. How do I reset my password?","z.B. Import aus alter Sperrliste":"e.g. Import from old block list","z.B. Junk, Spam oder INBOX.Junk":"e.g. Junk, Spam or INBOX.Junk","z.B. Tag der Deutschen Einheit":"e.g. German Unity Day","z.B. Technik-Postfach":"e.g. Technical mailbox","z.B. wiederholter Spam":"e.g. repeated spam","z.B. „In Besprechung bis 14:00“":"e.g. \"In a meeting until 14:00\"","zurückgeholt":"restored","zählen nur die dort eingetragenen Öffnungszeiten.":"only the opening hours entered there count.","{#} Artikel":"{} article(s)","{#} Aufrufe":"{} views","{#} Tag(e)":"{} day(s)","{#} Treffer":"{} hits","{} Abteilung(en) · {} Hilfethema/-themen":"{} department(s) · {} help topic(s)","{} Aktualisierung(en) an deinen Tickets":"{} update(s) to your tickets","{} Antworten von {} Abfragen":"{} answers from {} surveys","{} Artikel":"{} articles","{} Bedingung(en) ({})":"{} condition(s) ({})","{} Einträge · Seite {} von {}":"{} entries · page {} of {}","{} Einträge · Seite {}/{}":"{} entries · page {}/{}","{} Kontakte":"{} contacts","{} Mitarbeiter":"{} staff","{} Mitglied(er)":"{} member(s)","{} Std":"{} hrs","{} Tickets":"{} tickets","{} ausgewählt":"{} selected","{} gesperrt, {} übersprungen.":"{} blocked, {} skipped.","{} neue(s) nicht zugewiesene(s) Ticket(s)":"{} new unassigned ticket(s)","{} offen":"{} open","{} von 5":"{} of 5","{} · erstellt am {}":"{1} · created {2}","{} · {} · erstellt {}":"{} · {} · created {}","{} überfällig":"{} overdue","{}: {} Ticket(s)":"{}: {} ticket(s)","{}× angewendet":"applied {}×","{}× genutzt · {}":"used {}× · {}","· Handeln":"· Act","· Hinweis":"· Notice","· In Ordnung":"· OK","· Leiter: {}":"· Lead: {}","· Letzte automatische Prüfung: Noch nie":"· Last automatic check: never","· Letzter Abruf: Noch nie":"· Last fetch: never","· Live-Chat {} · Wissensdatenbank im Portal {}":"· Live chat {} · Knowledge base in the portal {}","· Prüfen":"· Check","· fällig {}":"· due {}","· für alle":"· for everyone","· {} Fehler ({})":"· {} errors ({})","· über den DeskHub-Push-Dienst, es ist nichts einzurichten":"· via the DeskHub push service, nothing to set up","Ältere Backups werden automatisch gelöscht wenn das Limit erreicht wird.":"Older backups are deleted automatically when the limit is reached.","Änderungen speichern":"Save changes","Änderungen und Mailfehler":"Changes and failed emails","Änderungen und fehlgeschlagene Mails":"Changes and failed emails","Öffentlich":"Public","Öffentlich (für Kunden sichtbar)":"Public (visible to customers)","Öffentliche Antwort senden":"Send public reply","Öffnen Sie zuerst den persönlichen Ticket-Link aus der Ticket-E-Mail in diesem Browser. Anschließend können Sie das Ticket hier Ihrem Konto hinzufügen, wenn die Ticket-E-Mail-Adresse mit Ihrer Konto-E-Mail-Adresse übereinstimmt.":"First open the personal ticket link from the ticket email in this browser. You can then add the ticket to your account here if the ticket email address matches your account email address.","Öffnungszeiten je Wochentag":"Opening hours per weekday","Öffnungszeiten speichern":"Save opening hours","Öffnungszeiten und Feiertage":"Opening hours and holidays","Ø Bewertung · {} Antworten von {} Abfragen":"Ø rating · {} answers from {} surveys","Ø Erste Antwort":"Ø First response","Ø Erstreaktion":"Ø First response","Ø Lösungszeit":"Ø Resolution time","Überfällig":"Overdue","Überfällig seit …":"Overdue since …","Übergeordnetes Thema":"Parent topic","Überschrift":"Heading","Übersicht":"Overview","– Abteilung wählen –":"– choose department –","– Download nur im Web":"– Download on the web only","– Hilfe-Thema wählen –":"– choose help topic –","– Interne Notizen, sichtbar für alle Administratoren":"– Internal notes, visible to all administrators","– Mitarbeiter wählen –":"– choose staff member –","– Priorität wählen –":"– choose priority –","– Standard-Mitarbeiterrechte –":"– default staff permissions –","– Textbaustein wählen –":"– choose text snippet –","– Vorlage wählen –":"– choose template –","– auswählen –":"– select –","– bitte wählen –":"– please choose –","– kein Leiter –":"– no lead –","– kein Thema –":"– no topic –","– kein bestimmter Dienst –":"– no specific service –","– keine Änderung –":"– no change –","– keine –":"– none –","– nicht vorgeben –":"– do not preset –","– nicht zugewiesen –":"– unassigned –","– nicht ändern –":"– do not change –","– ohne Vorlage –":"– without template –","– wählen –":"– choose –","— Abteilung auswählen —":"— select department —","— Automatisch (Standardadresse der Abteilung) —":"— Automatic (department default address) —","— Automatisch —":"— Automatic —","— Bitte wählen —":"— Please choose —","— Keine(r) —":"— None —","— Niemand —":"— Nobody —","— Oberste Hilfethemenebene —":"— Top help topic level —","— Standard —":"— Default —","— Standardabteilung (bzw. Abteilung des gewählten Themas) —":"— Default department (or department of the chosen topic) —","— Systemstandard —":"— System default —","— Zeiten der Priorität verwenden —":"— Use the priority's times —","— keine —":"— none —","— nicht ändern —":"— do not change —","— niemand —":"— nobody —","— ohne Vorlage —":"— without template —","‹ Liste":"‹ List","‹ Verwaltung":"‹ Administration","‹ Zurück":"‹ Back","ℹ️ Ausführung hochgeladener Dateien":"ℹ️ Execution of uploaded files","ℹ️ Erreichbarkeit interner Dateien":"ℹ️ Accessibility of internal files","ℹ️ Hinweise":"ℹ️ Notes","← Einstellungen":"← Settings","← Zum Kundenportal":"← To the customer portal","← Zur Anmeldung":"← Back to sign in","← Zur Ticket-Liste":"← To the ticket list","← Zurück":"← Back","↑ Priorität: {}":"↑ Priority: {}","→ bleiben erhalten":"→ are kept","↓ CSV exportieren":"↓ Export CSV","↩️ Spam-Markierung aufheben":"↩️ Remove spam mark","⏰ Automatischer Zeitplan":"⏰ Automatic schedule","⏱ Zeiterfassung":"⏱ Time tracking","⏱ Zeiterfassung läuft":"⏱ Time tracking running","○ Noch kein Postfach eingerichtet":"○ No mailbox set up yet","○ deaktiviert":"○ disabled","⚙ Einstellungen":"⚙ Settings","⚠️ Administratoren ohne Zwei-Faktor-Anmeldung":"⚠️ Administrators without two-factor authentication","⚠️ Datenbank: SQLite":"⚠️ Database: SQLite","⚠️ Noch kein geprüftes Backup":"⚠️ No verified backup yet","⛔ Absender sperren":"⛔ Block sender","⛔ Gesperrt":"⛔ Blocked","✅ Verschlüsselungsschlüssel gesetzt":"✅ Encryption key set","✍ Signatur":"✍ Signature","✓ Berichte einsehen":"✓ View reports","✓ Die Datenbank ist aktuell":"✓ The database is up to date","✓ Interne Notizen schreiben":"✓ Write internal notes","✓ Push ist aktiv":"✓ Push is active","✓ Tickets beantworten":"✓ Reply to tickets","✓ Tickets im Namen von Kunden erstellen":"✓ Create tickets on behalf of customers","✓ Tickets schließen/lösen":"✓ Close/resolve tickets","✓ Tickets zuweisen (Mitarbeiter/Team)":"✓ Assign tickets (staff/team)","✓ Tickets übertragen (Abteilung ändern)":"✓ Transfer tickets (change department)","✕ Abteilungen verwalten":"✕ Manage departments","✕ Admin-Protokoll einsehen":"✕ View admin log","✕ Digitalen Assistenten verwalten":"✕ Manage digital assistant","✕ E-Mail-Adressen verwalten":"✕ Manage email addresses","✕ E-Mail-Vorlagen verwalten":"✕ Manage email templates","✕ Eskalationsregeln verwalten":"✕ Manage escalation rules","✕ Filter zurücksetzen":"✕ Reset filters","✕ Geschäftszeiten verwalten":"✕ Manage business hours","✕ Hilfe-Themen verwalten":"✕ Manage help topics","✕ Mitarbeiter verwalten":"✕ Manage staff","✕ Organisationen verwalten":"✕ Manage organizations","✕ Prioritäten / SLA verwalten":"✕ Manage priorities / SLA","✕ Rollen verwalten":"✕ Manage roles","✕ Teams verwalten":"✕ Manage teams","✕ Textbausteine verwalten":"✕ Manage text snippets","✕ Tickets löschen":"✕ Delete tickets","✕ Wissensdatenbank verwalten":"✕ Manage knowledge base","✕ Zeiterfassung einsehen":"✕ View time tracking","❌ Installationsassistent ist nicht gesperrt":"❌ The installation wizard is not locked","❌ Website-URL nutzt kein HTTPS":"❌ Website URL does not use HTTPS","⬆️ Hochladen & installieren":"⬆️ Upload & install","🌐 Im Browser":"🌐 In browser","🌐 Web (im Namen)":"🌐 Web (on behalf)","🏖 Urlaub":"🏖 Vacation","👁 Beobachten":"👁 Watch","👁 Beobachtet":"👁 Watched","👁 Nicht mehr beobachten":"👁 Stop watching","👁 Ticket beobachten":"👁 Watch ticket","👋 Hallo! Wie können wir Ihnen helfen?":"👋 Hello! How can we help you?","👤 Konto":"👤 Account","💤 Auf später":"💤 Snooze","💤 Ausgeblendet bis {} Uhr, dann kommt das Ticket zurück.":"💤 Hidden until {}, then the ticket comes back.","💤 Wiedervorlage":"💤 Snooze","💤 bis":"💤 until","💬 Live-Chat":"💬 Live chat","💾 Backup jetzt erstellen":"💾 Create backup now","📄 Impressum":"📄 Legal notice","📎 Datei":"📎 File","📞 Telefon":"📞 Phone","📡 FTP-Upload":"📡 FTP upload","📥 osTicket Import":"📥 osTicket import","📧 E-Mail":"📧 Email","📧 Mitarbeiter":"📧 Staff","📱 Android-App: Push-Benachrichtigungen":"📱 Android app: push notifications","📱 Android-App: Welche Popups?":"📱 Android app: which popups?","🔄 Automatischer Hintergrund-Betrieb (ohne Server-Cronjob)":"🔄 Automatic background operation (without server cron job)","🔄 Erneut prüfen":"🔄 Check again","🔄 System-Update":"🔄 System update","🔒 Datenschutz":"🔒 Privacy","🔒 Datenschutzerklärung":"🔒 Privacy policy","🔒 Dieses Ticket wird gerade von „{}“ bearbeitet. Als Administrator kannst du es trotzdem lesen und bearbeiten.":"🔒 This ticket is currently being handled by \"{}\". As administrator you can still read and edit it.","🔒 Sicherheit":"🔒 Security","🔒 Wird gerade von {} bearbeitet – Sie sehen live mit, können hier aber nicht schreiben.":"🔒 Currently being handled by {} – you can watch live but cannot write here.","🔓 freigeben":"🔓 release","🔔 Meine Erwähnungen":"🔔 My mentions","🔴 Beschäftigt":"🔴 Busy","🔴 Überfällig":"🔴 Overdue","🖥 Intern":"🖥 Internal","🖥 Vor Ort / Intern":"🖥 On site / internal","🖨 Drucken":"🖨 Print","🖨 Drucken / Als PDF speichern":"🖨 Print / Save as PDF","🖨️ Ticket drucken":"🖨️ Print ticket","🖼️ Bild einfügen":"🖼️ Insert image","🗄 Datenbank aktualisieren":"🗄 Update database","🗑 Ticket löschen":"🗑 Delete ticket","🚫 Als Spam markieren":"🚫 Mark as spam","🚫 Sperren":"🚫 Block","🟡 Bald fällig":"🟡 Due soon","🟡 Bald fällig (< 4 Std.)":"🟡 Due soon (< 4 hrs)","🟡 Pause":"🟡 Break","🟢 Im Zeitplan":"🟢 On schedule","🟢 Verfügbar":"🟢 Available","+ Ansicht speichern":"+ Save view"} \ No newline at end of file diff --git a/mobile/src/main/java/de/deskhub/staff/DeskHubWidget.kt b/mobile/src/main/java/de/deskhub/staff/DeskHubWidget.kt index a5cffc1a..25c78478 100644 --- a/mobile/src/main/java/de/deskhub/staff/DeskHubWidget.kt +++ b/mobile/src/main/java/de/deskhub/staff/DeskHubWidget.kt @@ -93,6 +93,7 @@ class DeskHubWidget : AppWidgetProvider() { } private fun paint(context: Context, manager: AppWidgetManager, ids: IntArray, s: Snapshot) { + de.deskhub.staff.ui.I18n.init(context) ids.forEach { manager.updateAppWidget(it, views(context, s, layoutFor(manager.getAppWidgetOptions(it)))) } } @@ -139,7 +140,7 @@ class DeskHubWidget : AppWidgetProvider() { v.setTextViewText(R.id.widget_overdue, s.overdue.toString()) v.setTextViewText(R.id.widget_unanswered, s.unanswered.toString()) v.setInt(R.id.widget_tile_overdue, "setBackgroundResource", if (s.overdue > 0) R.drawable.widget_tile_alert else R.drawable.widget_tile) - v.setTextViewText(R.id.widget_time, "Stand " + SimpleDateFormat("HH:mm", Locale.GERMANY).format(Date(s.at))) + v.setTextViewText(R.id.widget_time, context.getString(R.string.widget_as_of, SimpleDateFormat("HH:mm", Locale.getDefault()).format(Date(s.at)))) v.setViewVisibility(R.id.widget_message, View.GONE) v.setViewVisibility(R.id.widget_tiles, View.VISIBLE) listOf(R.id.widget_t1, R.id.widget_t2, R.id.widget_t3).forEachIndexed { index, id -> @@ -153,7 +154,7 @@ class DeskHubWidget : AppWidgetProvider() { } } } else { - v.setTextViewText(R.id.widget_message, s.message ?: "Lade …") + v.setTextViewText(R.id.widget_message, s.message?.let { de.deskhub.staff.ui.I18n.tr(it) } ?: context.getString(R.string.widget_loading)) v.setViewVisibility(R.id.widget_message, View.VISIBLE) v.setViewVisibility(R.id.widget_tiles, View.INVISIBLE) v.setTextViewText(R.id.widget_time, "") diff --git a/mobile/src/main/java/de/deskhub/staff/MainActivity.kt b/mobile/src/main/java/de/deskhub/staff/MainActivity.kt index 8c3b8197..073e78fe 100644 --- a/mobile/src/main/java/de/deskhub/staff/MainActivity.kt +++ b/mobile/src/main/java/de/deskhub/staff/MainActivity.kt @@ -40,6 +40,7 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + de.deskhub.staff.ui.I18n.init(applicationContext) handleNotificationIntent(intent) setContent { val state by viewModel.state.collectAsState() 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 e1cc0f84..d3c38270 100644 --- a/mobile/src/main/java/de/deskhub/staff/data/TicketRepository.kt +++ b/mobile/src/main/java/de/deskhub/staff/data/TicketRepository.kt @@ -234,6 +234,13 @@ class TicketRepository( return requireApi().updateTicket(request).ticket.toDomain() } + suspend fun collaboratorAction(number: String, action: String, email: String? = null, id: Int? = null) = + requireApi().collaboratorAction(number, action, email, id).collaborators + suspend fun ticketTemplates() = requireApi().ticketTemplates().templates + suspend fun csatStats(days: Int) = requireApi().csatStats(days) + suspend fun statusPage() = requireApi().statusPage() + suspend fun statusAction(request: Map) = requireApi().statusAction(request) + suspend fun assistantOverview() = requireApi().assistantOverview() suspend fun assistantAnswers() = requireApi().assistantAnswers().items suspend fun assistantLog(query: Map) = requireApi().assistantLog(query) @@ -798,7 +805,7 @@ class TicketRepository( } private fun requireApi(): DeskHubApi = - checkNotNull(api) { "Keine aktive deskhub-Sitzung." } + checkNotNull(api) { "Keine aktive DeskHub-Sitzung." } } private fun StaffDto.toDomain() = Staff(id, name, email, role, permissions) @@ -912,6 +919,9 @@ private fun TicketDetailDto.toDomain() = TicketDetail( following = following, ), similar = similar?.let { s -> SimilarTickets(s.articles.map { it.title to it.url }, s.tickets.map { it.number to it.subject }) }, + collaborators = collaborators.map { de.deskhub.staff.domain.model.Collaborator(it.id, it.email, it.name) }, + followers = followers.map { it.name }, + csat = csat?.let { de.deskhub.staff.domain.model.Csat(it.rating, it.comment, it.sentAt, it.answeredAt) }, source = source, firstResponseDueAt = firstResponseDueAt, resolutionDueAt = resolutionDueAt, 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 f4ecfe7f..5c04909b 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 @@ -521,6 +521,26 @@ class DeskHubApi( suspend fun availability(): AvailabilityResponse = execute("availability.php", "GET", null, AvailabilityResponse::class.java) + // ─── Cc-Empfänger, Vorlagen, Zufriedenheit, Statusseite ────────────────── + + suspend fun collaboratorAction(number: String, action: String, email: String? = null, id: Int? = null): de.deskhub.staff.data.remote.dto.CollaboratorsResponse = + execute( + "ticket_collaborators.php", "POST", mapOf("number" to number, "action" to action, "email" to email, "id" to id), + de.deskhub.staff.data.remote.dto.CollaboratorsResponse::class.java, + ) + + suspend fun ticketTemplates(): de.deskhub.staff.data.remote.dto.TicketTemplatesResponse = + execute("ticket_templates.php", "GET", null, de.deskhub.staff.data.remote.dto.TicketTemplatesResponse::class.java) + + suspend fun csatStats(days: Int): de.deskhub.staff.data.remote.dto.CsatResponse = + execute("csat.php", "GET", null, de.deskhub.staff.data.remote.dto.CsatResponse::class.java, query = mapOf("days" to days.toString())) + + suspend fun statusPage(): de.deskhub.staff.data.remote.dto.StatusResponse = + execute("status.php", "GET", null, de.deskhub.staff.data.remote.dto.StatusResponse::class.java) + + suspend fun statusAction(request: Map): de.deskhub.staff.data.remote.dto.StatusResponse = + execute("status.php", "POST", request, de.deskhub.staff.data.remote.dto.StatusResponse::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) 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 ec99215b..e8bc8c2a 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 @@ -179,6 +179,10 @@ data class TicketDetailDto( val following: Boolean = false, // Passende Artikel und ähnliche gelöste Tickets; null bei älteren Servern val similar: SimilarDto? = null, + // Cc-Empfänger, beobachtende Mitarbeiter und Bewertung des Kunden; leer/null bei älteren Servern + val collaborators: List = emptyList(), + val followers: List = emptyList(), + val csat: CsatDto? = null, ) data class SimilarArticleDto(val title: String = "", val url: String = "") diff --git a/mobile/src/main/java/de/deskhub/staff/data/remote/dto/ExtraDtos.kt b/mobile/src/main/java/de/deskhub/staff/data/remote/dto/ExtraDtos.kt new file mode 100644 index 00000000..dee75ffe --- /dev/null +++ b/mobile/src/main/java/de/deskhub/staff/data/remote/dto/ExtraDtos.kt @@ -0,0 +1,118 @@ +package de.deskhub.staff.data.remote.dto + +// ── Cc-Empfänger, Beobachter, Zufriedenheitsabfrage ────────────────────────── + +data class CollaboratorDto( + val id: Int = 0, + val email: String = "", + val name: String = "", +) + +data class CollaboratorsResponse(val collaborators: List = emptyList()) + +data class FollowerDto( + val id: Int = 0, + val name: String = "", +) + +/** Bewertung des Kunden zu einem Ticket; rating = null, solange der Kunde nicht geantwortet hat. */ +data class CsatDto( + val rating: Int? = null, + val comment: String = "", + val sentAt: String? = null, + val answeredAt: String? = null, +) + +data class CsatDistDto(val rating: Int = 0, val count: Int = 0) + +data class CsatStaffDto(val id: Int = 0, val name: String = "", val count: Int = 0, val avg: Double = 0.0) + +data class CsatRecentDto( + val rating: Int = 0, + val comment: String = "", + val answeredAt: String = "", + val number: String = "", + val subject: String = "", +) + +data class CsatStatsDto( + val sent: Int = 0, + val answered: Int = 0, + val avg: Double? = null, + val rate: Double? = null, + val dist: List = emptyList(), + val byStaff: List = emptyList(), + val recent: List = emptyList(), +) + +data class CsatResponse( + val enabled: Boolean = false, + val days: Int = 30, + val stats: CsatStatsDto = CsatStatsDto(), +) + +// ── Ticket-Vorlagen ────────────────────────────────────────────────────────── + +data class TicketTemplateDto( + val id: Int = 0, + val name: String = "", + val description: String = "", + val subject: String = "", + val body: String = "", + val departmentId: Int? = null, + val helpTopicId: Int? = null, + val priorityId: Int? = null, + val isPublic: Boolean = true, +) + +data class TicketTemplatesResponse(val templates: List = emptyList()) + +// ── Statusseite ────────────────────────────────────────────────────────────── + +data class StatusOverallDto(val key: String = "operational", val label: String = "", val color: String = "#157a45") + +data class StatusStateDto(val key: String = "", val label: String = "") + +data class StatusComponentDto( + val id: Int = 0, + val name: String = "", + val description: String = "", + val status: String = "operational", + val statusLabel: String = "", +) + +data class StatusUpdateDto( + val status: String = "", + val statusLabel: String = "", + val body: String = "", + val createdAt: String = "", +) + +data class StatusIncidentDto( + val id: Int = 0, + val title: String = "", + val status: String = "", + val statusLabel: String = "", + val component: String? = null, + val componentId: Int? = null, + val createdAt: String = "", + val updatedAt: String = "", + val resolvedAt: String? = null, + val updates: List = emptyList(), +) + +data class StatusSummaryDto( + val enabled: Boolean = false, + val title: String = "Systemstatus", + val url: String = "", + val overall: StatusOverallDto = StatusOverallDto(), + val componentStates: List = emptyList(), + val incidentStates: List = emptyList(), + val components: List = emptyList(), + val incidents: List = emptyList(), +) + +data class StatusResponse( + val status: StatusSummaryDto = StatusSummaryDto(), + val canManage: Boolean = false, +) diff --git a/mobile/src/main/java/de/deskhub/staff/domain/model/ExtraModels.kt b/mobile/src/main/java/de/deskhub/staff/domain/model/ExtraModels.kt new file mode 100644 index 00000000..97c8e8f5 --- /dev/null +++ b/mobile/src/main/java/de/deskhub/staff/domain/model/ExtraModels.kt @@ -0,0 +1,7 @@ +package de.deskhub.staff.domain.model + +/** Cc-Empfänger eines Tickets: bekommt die öffentlichen Antworten per E-Mail in Kopie. */ +data class Collaborator(val id: Int, val email: String, val name: String) + +/** Bewertung des Kunden nach dem Schließen; rating = null, solange keine Antwort da ist. */ +data class Csat(val rating: Int?, val comment: String, val sentAt: String?, val answeredAt: 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 32606b17..2f8d0559 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 @@ -94,6 +94,10 @@ data class TicketDetail( val canUnlock: Boolean = false, /** Passende Wissensdatenbank-Artikel und ähnliche gelöste Tickets (null bei älteren Servern). */ val similar: SimilarTickets? = null, + /** Cc-Empfänger, beobachtende Mitarbeiter (Namen) und die Bewertung des Kunden (null = keine Abfrage). */ + val collaborators: List = emptyList(), + val followers: List = emptyList(), + val csat: Csat? = null, ) data class SimilarTickets( diff --git a/mobile/src/main/java/de/deskhub/staff/ui/AssistantAdmin.kt b/mobile/src/main/java/de/deskhub/staff/ui/AssistantAdmin.kt index 37ac79a9..67dafcb3 100644 --- a/mobile/src/main/java/de/deskhub/staff/ui/AssistantAdmin.kt +++ b/mobile/src/main/java/de/deskhub/staff/ui/AssistantAdmin.kt @@ -32,7 +32,6 @@ import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Switch import androidx.compose.material3.Tab -import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults @@ -72,7 +71,7 @@ fun AdminAssistantScreen(state: AssistantUiState, viewModel: DeskHubViewModel, o } } LaunchedEffect(state.message) { - state.message?.let { snackbar.showSnackbar(it); viewModel.assistantClearMessage() } + state.message?.let { snackbar.showSnackbar(I18n.tr(it)); viewModel.assistantClearMessage() } } Scaffold( 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 e55c6c42..fd1a2b51 100644 --- a/mobile/src/main/java/de/deskhub/staff/ui/DeskHubApp.kt +++ b/mobile/src/main/java/de/deskhub/staff/ui/DeskHubApp.kt @@ -109,7 +109,6 @@ import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Surface import androidx.compose.material3.Switch import androidx.compose.material3.Tab -import androidx.compose.material3.Text import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.graphics.toArgb import androidx.compose.material.icons.filled.DarkMode @@ -301,6 +300,7 @@ private fun StaffNavigation(state: DeskHubState, viewModel: DeskHubViewModel) { onLoad = { viewModel.loadDashboard() viewModel.loadAvailability() + viewModel.loadCsat() }, onSetStatus = viewModel::setMyAvailability, onTicket = { number -> @@ -371,6 +371,7 @@ private fun StaffNavigation(state: DeskHubState, viewModel: DeskHubViewModel) { NewTicketScreen( state = state, onBack = { navController.popBackStack() }, + onLoadTemplates = viewModel::loadTemplates, onCreate = { requesterName, requesterEmail, subject, message, departmentId, priorityId, assignedStaffId, notify -> viewModel.createTicket( requesterName, requesterEmail, subject, message, @@ -394,6 +395,8 @@ private fun StaffNavigation(state: DeskHubState, viewModel: DeskHubViewModel) { onSpamAction = viewModel::setTicketSpam, onSnooze = viewModel::snoozeTicket, onFollow = viewModel::setFollowing, + onAddCc = viewModel::addCollaborator, + onRemoveCc = viewModel::removeCollaborator, onUnlock = { state.selectedTicket?.summary?.number?.let(viewModel::unlockTicket) }, ) } @@ -452,6 +455,9 @@ private fun StaffNavigation(state: DeskHubState, viewModel: DeskHubViewModel) { onSetMemberStatus = viewModel::setMemberAvailability, ) } + composable("adminStatus") { + AdminStatusScreen(state = state.statusPage, viewModel = viewModel, onBack = { navController.popBackStack() }) + } composable("adminAssistant") { AdminAssistantScreen(state = state.assistant, viewModel = viewModel, onBack = { navController.popBackStack() }) } @@ -676,6 +682,7 @@ internal fun LoginScreen(state: DeskHubState, viewModel: DeskHubViewModel) { .background(LocalDeskExtras.current.gradient), contentAlignment = Alignment.Center, ) { + LanguageFlags(Modifier.align(Alignment.TopEnd).statusBarsPadding().padding(12.dp), onDark = true) Column(horizontalAlignment = Alignment.CenterHorizontally) { Box( modifier = Modifier @@ -1336,6 +1343,7 @@ private fun DashboardScreen( StatTile("SLA-Quote", stats.slaPercent?.let { "${it.toInt()}%" } ?: "–", Modifier.weight(1f)) } } + state.csat?.takeIf { it.enabled || it.stats.sent > 0 }?.let { csat -> item { CsatCard(csat) } } if (stats.recent.isNotEmpty()) { item { SectionTitle("Priorisiert: mir zugewiesen oder unbearbeitet") } items(stats.recent, key = TicketSummary::id) { ticket -> TicketCard(ticket, state.metadata, onTicket) } @@ -1450,7 +1458,7 @@ private fun AdminMenuRow(icon: androidx.compose.ui.graphics.vector.ImageVector, } @Composable -private fun AdminTopBar(title: String, onBack: () -> Unit) { +internal fun AdminTopBar(title: String, onBack: () -> Unit) { TopAppBar( title = { Text(title, fontWeight = FontWeight.Bold) }, navigationIcon = { @@ -3402,8 +3410,8 @@ private fun AdminReportsScreen( fun format(value: Any?, unit: String): String { val n = (value as? Number)?.toDouble() ?: return "–" return when (unit) { - "%" -> String.format(java.util.Locale.GERMANY, "%.1f %%", n) - "Std" -> String.format(java.util.Locale.GERMANY, "%.1f Std", n) + "%" -> String.format(I18n.locale(), "%.1f %%", n) + "Std" -> String.format(I18n.locale(), "%.1f ${if (I18n.lang == "en") "hrs" else "Std"}", n) else -> n.toLong().toString() } } @@ -5124,7 +5132,11 @@ private fun NewTicketScreen( state: DeskHubState, onBack: () -> Unit, onCreate: (String, String, String, String, Int, Int, Int?, Boolean) -> Unit, + onLoadTemplates: () -> Unit = {}, ) { + LaunchedEffect(Unit) { onLoadTemplates() } + var templateName by rememberSaveable { mutableStateOf("") } + var templateExpanded by remember { mutableStateOf(false) } var name by rememberSaveable { mutableStateOf(state.newTicketPrefillName) } var email by rememberSaveable { mutableStateOf(state.newTicketPrefillEmail) } var subject by rememberSaveable { mutableStateOf("") } @@ -5159,6 +5171,31 @@ private fun NewTicketScreen( .padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp), ) { + // Ticket-Vorlagen füllen Betreff, Text, Abteilung und Priorität vor + if (state.templates.isNotEmpty()) { + SelectionField( + label = "Vorlage", + value = templateName.ifBlank { "– ohne Vorlage –" }, + expanded = templateExpanded, + onExpand = { templateExpanded = true }, + onDismiss = { templateExpanded = false }, + ) { + DropdownMenuItem(text = { Text("– ohne Vorlage –") }, onClick = { templateName = ""; templateExpanded = false }) + state.templates.forEach { t -> + DropdownMenuItem( + text = { Text(t.name) }, + onClick = { + templateName = t.name + subject = t.subject + message = t.body + t.departmentId?.let { id -> if (departments.any { it.id == id }) departmentId = id } + t.priorityId?.let { id -> if (priorities.any { it.id == id }) priorityId = id } + templateExpanded = false + }, + ) + } + } + } OutlinedTextField( value = name, onValueChange = { name = it }, @@ -5269,6 +5306,8 @@ private fun TicketDetailScreen( onSpamAction: (Boolean) -> Unit, onSnooze: (String?) -> Unit = {}, onFollow: (Boolean) -> Unit = {}, + onAddCc: (String) -> Unit = {}, + onRemoveCc: (Int) -> Unit = {}, onUnlock: () -> Unit = {}, ) { Scaffold( @@ -5301,6 +5340,8 @@ private fun TicketDetailScreen( onSpamAction = onSpamAction, onSnooze = onSnooze, onFollow = onFollow, + onAddCc = onAddCc, + onRemoveCc = onRemoveCc, onUnlock = onUnlock, ) else -> Box(Modifier.padding(padding)) { EmptyState("Kein Ticket ausgewählt.") } @@ -5321,6 +5362,8 @@ private fun TicketDetailContent( onSpamAction: (Boolean) -> Unit, onSnooze: (String?) -> Unit = {}, onFollow: (Boolean) -> Unit = {}, + onAddCc: (String) -> Unit = {}, + onRemoveCc: (Int) -> Unit = {}, onUnlock: () -> Unit = {}, ) { var selectedStatus by remember(ticket.summary.number) { mutableStateOf(ticket.summary.status) } @@ -5517,6 +5560,60 @@ private fun TicketDetailContent( } } } + ticket.csat?.let { csat -> + item { + DeskCard { + Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) { + Text("Kundenzufriedenheit", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold) + if (csat.rating != null) { + Text("★".repeat(csat.rating) + "☆".repeat(5 - csat.rating) + " ${csat.rating} von 5", style = MaterialTheme.typography.titleMedium, color = Color(0xFFF5B301)) + if (csat.comment.isNotBlank()) Text("„${csat.comment}“", style = MaterialTheme.typography.bodyMedium) + csat.answeredAt?.let { Text("Bewertet am ${it.take(16).replace('T', ' ')}", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) } + } else { + Text("Abfrage am ${csat.sentAt.orEmpty().take(16).replace('T', ' ')} gesendet, noch keine Antwort.", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + } + } + } + } + item { + var ccInput by rememberSaveable(ticket.summary.number) { mutableStateOf("") } + DeskCard { + Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text("Beobachter und Cc", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold) + Text("Beobachter", style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.SemiBold) + Text( + if (ticket.followers.isEmpty()) "Niemand. Beobachter bekommen Benachrichtigungen, ohne dass ihnen das Ticket zugewiesen ist." else ticket.followers.joinToString(", "), + style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text("Cc-Empfänger", style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.SemiBold) + if (ticket.collaborators.isEmpty()) { + Text("Niemand. Cc-Empfänger bekommen die öffentlichen Antworten per E-Mail in Kopie.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + ticket.collaborators.forEach { c -> + Row(verticalAlignment = Alignment.CenterVertically) { + Text(if (c.name.isNotBlank()) "${c.name} <${c.email}>" else c.email, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f)) + if (canReply) IconButton(onClick = { onRemoveCc(c.id) }, enabled = !mutationLoading) { Icon(Icons.Filled.Close, contentDescription = "Cc entfernen") } + } + } + if (canReply) { + OutlinedTextField( + value = ccInput, + onValueChange = { ccInput = it }, + label = { Text("Cc-Adresse hinzufügen") }, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email), + modifier = Modifier.fillMaxWidth(), + ) + OutlinedButton( + onClick = { onAddCc(ccInput); ccInput = "" }, + enabled = !mutationLoading && ccInput.contains("@"), + modifier = Modifier.fillMaxWidth(), + ) { Text("Cc hinzufügen") } + } + } + } + } item { DeskCard { Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { @@ -5949,7 +6046,7 @@ private fun ticketSourceLabel(source: String): String = when (source) { } @Composable -private fun LoadingState(message: String) { +internal fun LoadingState(message: String) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Column(horizontalAlignment = Alignment.CenterHorizontally) { CircularProgressIndicator(strokeWidth = 3.dp) @@ -5972,7 +6069,7 @@ private fun EmptyState(message: String) { } @Composable -private fun ErrorState(message: String, retry: () -> Unit) { +internal fun ErrorState(message: String, retry: () -> Unit) { Box(modifier = Modifier.fillMaxSize().padding(24.dp), contentAlignment = Alignment.Center) { Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(10.dp)) { Box(Modifier.size(64.dp).clip(CircleShape).background(MaterialTheme.colorScheme.errorContainer), contentAlignment = Alignment.Center) { 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 41d32a97..554fd9e2 100644 --- a/mobile/src/main/java/de/deskhub/staff/ui/DeskHubViewModel.kt +++ b/mobile/src/main/java/de/deskhub/staff/ui/DeskHubViewModel.kt @@ -73,6 +73,15 @@ data class AssistantUiState( val message: String? = null, ) +/** Zustand der Statusseite (Dienste und Störungsmeldungen für Kunden). */ +data class StatusUiState( + val data: de.deskhub.staff.data.remote.dto.StatusSummaryDto? = null, + val canManage: Boolean = false, + val loading: Boolean = false, + val error: String? = null, + val message: String? = null, +) + data class DeskHubState( val initializing: Boolean = true, val staff: Staff? = null, @@ -83,6 +92,10 @@ data class DeskHubState( val savedServerUrl: String? = null, val metadata: Metadata? = null, val assistant: AssistantUiState = AssistantUiState(), + /** Ticket-Vorlagen für „Neues Ticket“, Auswertung der Zufriedenheitsabfrage und Statusseite. */ + val templates: List = emptyList(), + val csat: de.deskhub.staff.data.remote.dto.CsatResponse? = null, + val statusPage: StatusUiState = StatusUiState(), /** Von außen angeforderter Bildschirm (zum Beispiel aus dem Widget): "newTicket" oder "tickets". */ val requestedRoute: String? = null, val cannedResponses: List = emptyList(), @@ -579,6 +592,62 @@ class DeskHubViewModel( } } + /** Cc-Empfänger zum geöffneten Ticket hinzufügen. */ + fun addCollaborator(email: String) = collaboratorChange { number -> repository.collaboratorAction(number, "add", email = email.trim()) } + + /** Cc-Empfänger vom geöffneten Ticket entfernen. */ + fun removeCollaborator(id: Int) = collaboratorChange { number -> repository.collaboratorAction(number, "remove", id = id) } + + private fun collaboratorChange(block: suspend (String) -> Unit) { + val ticket = state.value.selectedTicket ?: return + viewModelScope.launch { + mutableState.update { it.copy(mutationLoading = true, mutationError = null) } + runCatching { block(ticket.summary.number) } + .onSuccess { + mutableState.update { it.copy(mutationLoading = false) } + openTicket(ticket.summary.number) + } + .onFailure { error -> mutableState.update { it.copy(mutationLoading = false, mutationError = error.userMessage()) } } + } + } + + /** Ticket-Vorlagen laden (für den Bildschirm „Neues Ticket“). Fehler sind nicht schlimm: dann gibt es keine Auswahl. */ + fun loadTemplates() { + viewModelScope.launch { + runCatching { repository.ticketTemplates() }.onSuccess { list -> mutableState.update { it.copy(templates = list) } } + } + } + + /** Auswertung der Zufriedenheitsabfrage laden (für die Übersicht). */ + fun loadCsat(days: Int = 30) { + viewModelScope.launch { + runCatching { repository.csatStats(days) }.onSuccess { r -> mutableState.update { it.copy(csat = r) } } + } + } + + private fun updateStatusPage(block: (StatusUiState) -> StatusUiState) = mutableState.update { it.copy(statusPage = block(it.statusPage)) } + + fun statusLoad() { + viewModelScope.launch { + updateStatusPage { it.copy(loading = true, error = null) } + runCatching { repository.statusPage() } + .onSuccess { r -> updateStatusPage { it.copy(loading = false, data = r.status, canManage = r.canManage) } } + .onFailure { e -> updateStatusPage { it.copy(loading = false, error = e.userMessage()) } } + } + } + + /** Aktion auf der Statusseite (nur Administratoren), zum Beispiel Zustand eines Dienstes ändern oder eine Meldung veröffentlichen. */ + fun statusAct(request: Map, success: String) { + viewModelScope.launch { + updateStatusPage { it.copy(loading = true, error = null) } + runCatching { repository.statusAction(request) } + .onSuccess { r -> updateStatusPage { it.copy(loading = false, data = r.status, canManage = r.canManage, message = success) } } + .onFailure { e -> updateStatusPage { it.copy(loading = false, error = e.userMessage()) } } + } + } + + fun statusClearMessage() = updateStatusPage { it.copy(message = null, error = null) } + /** Markiert das aktuell geöffnete Ticket als Spam, oder macht das (restore = true) rückgängig. */ fun setTicketSpam(restore: Boolean) { val ticket = state.value.selectedTicket ?: return diff --git a/mobile/src/main/java/de/deskhub/staff/ui/DeskNav.kt b/mobile/src/main/java/de/deskhub/staff/ui/DeskNav.kt index 3050a8df..163602d5 100644 --- a/mobile/src/main/java/de/deskhub/staff/ui/DeskNav.kt +++ b/mobile/src/main/java/de/deskhub/staff/ui/DeskNav.kt @@ -49,7 +49,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Surface -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -138,6 +137,7 @@ fun MoreScreen(staff: Staff?, onNavigate: (String) -> Unit, onLogout: () -> Unit 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 (isFullAdmin) add(MoreEntry(Icons.Filled.Block, "Sperrliste", "Absender sperren", "adminBanlist")) + 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")) } @@ -152,6 +152,7 @@ fun MoreScreen(staff: Staff?, onNavigate: (String) -> Unit, onLogout: () -> Unit Text(staff?.email.orEmpty(), style = MaterialTheme.typography.bodySmall, color = Color.White.copy(alpha = 0.85f)) Text(if (isFullAdmin) "Administrator" else "Mitarbeiter", style = MaterialTheme.typography.labelMedium, color = Color.White.copy(alpha = 0.85f)) } + LanguageFlags(onDark = true) } } } diff --git a/mobile/src/main/java/de/deskhub/staff/ui/DeskTheme.kt b/mobile/src/main/java/de/deskhub/staff/ui/DeskTheme.kt index acffd661..b7b6c967 100644 --- a/mobile/src/main/java/de/deskhub/staff/ui/DeskTheme.kt +++ b/mobile/src/main/java/de/deskhub/staff/ui/DeskTheme.kt @@ -15,7 +15,6 @@ import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Shapes -import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarColors import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.Typography diff --git a/mobile/src/main/java/de/deskhub/staff/ui/I18n.kt b/mobile/src/main/java/de/deskhub/staff/ui/I18n.kt new file mode 100644 index 00000000..07003417 --- /dev/null +++ b/mobile/src/main/java/de/deskhub/staff/ui/I18n.kt @@ -0,0 +1,152 @@ +package de.deskhub.staff.ui + +import android.content.Context +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import java.util.Locale + +/** + * Sprachen der App: Deutsch (Quelltext) und English. + * Der Quelltext ist deutsch. Bei English übersetzt [tr] jeden Text über das Wörterbuch assets/lang_en.json + * (Schlüssel = deutscher Text, "{}" = beliebiger Teil, "{#}" = Zahl, "{1}"/"{2}" = Reihenfolge in der Übersetzung). + * Was nicht im Wörterbuch steht, bleibt deutsch. Alle Textfelder der App gehen über das übersetzende [Text] (siehe I18nText.kt). + * [lang] ist ein Compose-Zustand: nach dem Umschalten (Flagge) aktualisiert sich die Oberfläche sofort. + */ +object I18n { + private const val PREFS = "deskhub_i18n" + private var ready = false + private var exact: Map = emptyMap() + private var patterns: List> = emptyList() + private val spaces = Regex("\\s+") + + var lang by mutableStateOf("de") + private set + + /** Liest die gespeicherte Sprache (sonst die des Telefons) und das Wörterbuch. Mehrfacher Aufruf ist harmlos. */ + fun init(context: Context) { + if (ready) return + ready = true + val app = context.applicationContext + val saved = app.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getString("lang", null) + lang = if (saved == "de" || saved == "en") saved else if (Locale.getDefault().language == "de") "de" else "en" + runCatching { load(app.assets.open("lang_en.json").bufferedReader(Charsets.UTF_8).use { it.readText() }) } + } + + /** Liest das Wörterbuch (JSON-Objekt: deutscher Text -> English). */ + internal fun load(json: String) { + run { + val map: Map = Gson().fromJson(json, object : TypeToken>() {}.type) + val ex = HashMap() + val pa = ArrayList>() + for ((k, v) in map) { + if (v.isEmpty() || k.startsWith("_")) continue + val key = normalize(k) + if (key.contains("{}") || key.contains("{#}")) { + val sb = StringBuilder("^") + var i = 0 + while (i < key.length) { + when { + key.startsWith("{#}", i) -> { sb.append("(\\d+)"); i += 3 } + key.startsWith("{}", i) -> { sb.append("(.+?)"); i += 2 } + else -> { sb.append(Regex.escape(key[i].toString())); i++ } + } + } + sb.append("$") + pa.add(Regex(sb.toString(), RegexOption.DOT_MATCHES_ALL) to v) + } else { + ex[key] = v + } + } + exact = ex + patterns = pa + } + } + + internal fun useLanguage(code: String) { lang = code } + + fun set(context: Context, code: String) { + if (code != "de" && code != "en") return + context.applicationContext.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().putString("lang", code).apply() + lang = code + } + + private fun normalize(s: String) = s.replace(spaces, " ").trim() + + /** Übersetzt einen Text (bei Deutsch unverändert). Leerraum am Rand bleibt erhalten. */ + fun tr(s: String): String { + if (lang != "en" || s.isEmpty()) return s + val core = normalize(s) + if (core.isEmpty()) return s + val hit = exact[core] ?: patterns.firstNotNullOfOrNull { (re, v) -> + re.find(core)?.let { m -> + var seq = 0 + // Eingefügte Teile werden selbst noch einmal übersetzt (zum Beispiel „in 2 Std 5 Min“ in „Lösung fällig: {}“) + v.replace(Regex("\\{(\\d+)}")) { x -> tr(m.groupValues.getOrNull(x.groupValues[1].toInt()) ?: "") } + .replace("{}") { seq++; tr(m.groupValues.getOrNull(seq) ?: "") } + } + } ?: return s + val lead = s.takeWhile { it.isWhitespace() } + val trail = s.takeLastWhile { it.isWhitespace() } + return lead + hit + trail + } + + private fun String.replace(token: String, transform: () -> String): String { + val sb = StringBuilder() + var from = 0 + while (true) { + val i = indexOf(token, from) + if (i < 0) break + sb.append(this, from, i).append(transform()) + from = i + token.length + } + return sb.append(this, from, length).toString() + } + + /** Datumsformat-Sprache für Anzeigen, die die App selbst formatiert. */ + fun locale(): Locale = if (lang == "en") Locale.UK else Locale.GERMANY +} + +/** Kurzform zum Übersetzen in Kotlin-Code außerhalb von Text (zum Beispiel für Snackbar-Meldungen). */ +fun tr(s: String): String = I18n.tr(s) + +/** Flaggen zum Umschalten der Sprache (🇩🇪 / 🇬🇧). */ +@Composable +fun LanguageFlags(modifier: Modifier = Modifier, onDark: Boolean = false) { + val context = LocalContext.current + val current = I18n.lang + Row(modifier, horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { + for ((code, flag, name) in listOf(Triple("de", "🇩🇪", "Deutsch"), Triple("en", "🇬🇧", "English"))) { + val on = code == current + androidx.compose.material3.Text( + flag, + fontSize = 22.sp, + modifier = Modifier + .clip(RoundedCornerShape(8.dp)) + .border(BorderStroke(if (on) 2.dp else 0.dp, if (onDark) Color.White else Color(0xFF0F6F6A)), RoundedCornerShape(8.dp)) + .clickable { I18n.set(context, code) } + .padding(horizontal = 6.dp, vertical = 2.dp) + .semantics { contentDescription = name }, + ) + } + } +} diff --git a/mobile/src/main/java/de/deskhub/staff/ui/I18nText.kt b/mobile/src/main/java/de/deskhub/staff/ui/I18nText.kt new file mode 100644 index 00000000..7ea2123f --- /dev/null +++ b/mobile/src/main/java/de/deskhub/staff/ui/I18nText.kt @@ -0,0 +1,81 @@ +package de.deskhub.staff.ui + +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.TextUnit +import androidx.compose.material3.LocalTextStyle + +/** + * Übersetzendes Text-Element: Alle Dateien im Paket de.deskhub.staff.ui rufen dieses Text statt des von Material auf + * (der Import androidx.compose.material3.Text wurde entfernt, damit dieses hier gilt). Es übersetzt den Text über [I18n.tr] + * und reicht ihn sonst unverändert an Material weiter. + */ +@Composable +fun Text( + text: String, + modifier: Modifier = Modifier, + color: Color = Color.Unspecified, + fontSize: TextUnit = TextUnit.Unspecified, + fontStyle: FontStyle? = null, + fontWeight: FontWeight? = null, + fontFamily: FontFamily? = null, + letterSpacing: TextUnit = TextUnit.Unspecified, + textDecoration: TextDecoration? = null, + textAlign: TextAlign? = null, + lineHeight: TextUnit = TextUnit.Unspecified, + overflow: TextOverflow = TextOverflow.Clip, + softWrap: Boolean = true, + maxLines: Int = Int.MAX_VALUE, + minLines: Int = 1, + onTextLayout: ((TextLayoutResult) -> Unit)? = null, + style: TextStyle = LocalTextStyle.current, +) { + androidx.compose.material3.Text( + text = I18n.tr(text), modifier = modifier, color = color, fontSize = fontSize, fontStyle = fontStyle, fontWeight = fontWeight, + fontFamily = fontFamily, letterSpacing = letterSpacing, textDecoration = textDecoration, textAlign = textAlign, lineHeight = lineHeight, + overflow = overflow, softWrap = softWrap, maxLines = maxLines, minLines = minLines, onTextLayout = onTextLayout, style = style, + ) +} + +@Composable +fun Text( + text: AnnotatedString, + modifier: Modifier = Modifier, + color: Color = Color.Unspecified, + fontSize: TextUnit = TextUnit.Unspecified, + fontStyle: FontStyle? = null, + fontWeight: FontWeight? = null, + fontFamily: FontFamily? = null, + letterSpacing: TextUnit = TextUnit.Unspecified, + textDecoration: TextDecoration? = null, + textAlign: TextAlign? = null, + lineHeight: TextUnit = TextUnit.Unspecified, + overflow: TextOverflow = TextOverflow.Clip, + softWrap: Boolean = true, + maxLines: Int = Int.MAX_VALUE, + minLines: Int = 1, + inlineContent: Map = mapOf(), + onTextLayout: (TextLayoutResult) -> Unit = {}, + style: TextStyle = LocalTextStyle.current, +) { + // Nur reine Texte ohne Formatierung werden übersetzt; formatierte Texte bleiben, wie sie sind + val shown = if (text.spanStyles.isEmpty() && text.paragraphStyles.isEmpty() && I18n.lang == "en") buildAnnotatedString { append(I18n.tr(text.text)) } else text + androidx.compose.material3.Text( + text = shown, modifier = modifier, color = color, fontSize = fontSize, fontStyle = fontStyle, fontWeight = fontWeight, + fontFamily = fontFamily, letterSpacing = letterSpacing, textDecoration = textDecoration, textAlign = textAlign, lineHeight = lineHeight, + overflow = overflow, softWrap = softWrap, maxLines = maxLines, minLines = minLines, inlineContent = inlineContent, + onTextLayout = onTextLayout, style = style, + ) +} diff --git a/mobile/src/main/java/de/deskhub/staff/ui/PushPrefsScreen.kt b/mobile/src/main/java/de/deskhub/staff/ui/PushPrefsScreen.kt index 029c35e5..87c330a5 100644 --- a/mobile/src/main/java/de/deskhub/staff/ui/PushPrefsScreen.kt +++ b/mobile/src/main/java/de/deskhub/staff/ui/PushPrefsScreen.kt @@ -18,7 +18,6 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable diff --git a/mobile/src/main/java/de/deskhub/staff/ui/StatusPageAdmin.kt b/mobile/src/main/java/de/deskhub/staff/ui/StatusPageAdmin.kt new file mode 100644 index 00000000..d1b94450 --- /dev/null +++ b/mobile/src/main/java/de/deskhub/staff/ui/StatusPageAdmin.kt @@ -0,0 +1,220 @@ +package de.deskhub.staff.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Switch +import androidx.compose.material3.Button +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import de.deskhub.staff.data.remote.dto.CsatResponse +import de.deskhub.staff.data.remote.dto.StatusStateDto + +private fun statusColor(key: String): Color = when (key) { + "operational" -> Color(0xFF157A45) + "maintenance" -> Color(0xFF2563EB) + "degraded" -> Color(0xFFB45309) + "outage" -> Color(0xFFDC2626) + else -> Color(0xFF64748B) +} + +@Composable +private fun StatusPicker(label: String, value: String, options: List, emptyLabel: String? = null, onPick: (String) -> Unit) { + var open by remember { mutableStateOf(false) } + Box { + OutlinedButton(onClick = { open = true }, modifier = Modifier.fillMaxWidth()) { + Text("$label: " + (options.firstOrNull { it.key == value }?.label ?: emptyLabel ?: "–")) + } + androidx.compose.material3.DropdownMenu(expanded = open, onDismissRequest = { open = false }) { + if (emptyLabel != null) DropdownMenuItem(text = { Text(emptyLabel) }, onClick = { onPick(""); open = false }) + options.forEach { o -> DropdownMenuItem(text = { Text(o.label) }, onClick = { onPick(o.key); open = false }) } + } + } +} + +/** Statusseite für Kunden: Dienste, Zustände und Störungsmeldungen. Ändern dürfen nur Administratoren. */ +@Composable +fun AdminStatusScreen(state: StatusUiState, viewModel: DeskHubViewModel, onBack: () -> Unit) { + val snackbar = remember { SnackbarHostState() } + LaunchedEffect(Unit) { viewModel.statusLoad() } + LaunchedEffect(state.message, state.error) { + (state.message ?: state.error)?.let { snackbar.showSnackbar(I18n.tr(it)); viewModel.statusClearMessage() } + } + val data = state.data + var title by rememberSaveable { mutableStateOf("") } + var body by rememberSaveable { mutableStateOf("") } + var level by rememberSaveable { mutableStateOf("investigating") } + var componentId by rememberSaveable { mutableStateOf("") } + var componentState by rememberSaveable { mutableStateOf("") } + + Scaffold( + topBar = { AdminTopBar("Statusseite", onBack) }, + snackbarHost = { SnackbarHost(snackbar) }, + ) { padding -> + when { + state.loading && data == null -> Box(Modifier.padding(padding)) { LoadingState("Statusseite wird geladen …") } + data == null -> Box(Modifier.padding(padding)) { ErrorState(state.error ?: "Keine Daten verfügbar.", viewModel::statusLoad) } + else -> LazyColumn( + modifier = Modifier.padding(padding).fillMaxSize(), + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + item { + DeskCard { + Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text(data.overall.label, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = statusColor(data.overall.key)) + Row(verticalAlignment = Alignment.CenterVertically) { + Text("Statusseite für Kunden anzeigen", Modifier.weight(1f), style = MaterialTheme.typography.bodyMedium) + Switch(checked = data.enabled, enabled = state.canManage && !state.loading, onCheckedChange = { on -> + viewModel.statusAct(mapOf("action" to "set_enabled", "enabled" to on), if (on) "Statusseite eingeschaltet" else "Statusseite ausgeschaltet") + }) + } + if (data.enabled && data.url.isNotBlank()) Text("Adresse für Kunden: ${data.url}", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + if (!state.canManage) Text("Nur Administratoren dürfen die Statusseite ändern.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + } + } + item { SectionTitle("Dienste") } + if (data.components.isEmpty()) item { Text("Noch keine Dienste eingetragen.", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) } + items(data.components, key = { it.id }) { c -> + DeskCard { + Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) { + Text(c.name, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold) + if (c.description.isNotBlank()) Text(c.description, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + if (state.canManage) { + StatusPicker("Zustand", c.status, data.componentStates) { v -> + if (v.isNotBlank()) viewModel.statusAct(mapOf("action" to "set_component", "id" to c.id, "status" to v), "Zustand geändert") + } + } else { + Text(c.statusLabel, color = statusColor(c.status), fontWeight = FontWeight.SemiBold) + } + } + } + } + if (state.canManage) { + item { SectionTitle("Neue Meldung (Störung oder Wartung)") } + item { + DeskCard { + Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + OutlinedTextField(value = title, onValueChange = { title = it }, label = { Text("Titel") }, singleLine = true, modifier = Modifier.fillMaxWidth()) + OutlinedTextField(value = body, onValueChange = { body = it }, label = { Text("Beschreibung") }, minLines = 3, modifier = Modifier.fillMaxWidth()) + StatusPicker("Stufe", level, data.incidentStates) { if (it.isNotBlank()) level = it } + StatusPicker( + "Betrifft", componentId, + data.components.map { StatusStateDto(it.id.toString(), it.name) }, emptyLabel = "kein bestimmter Dienst", + ) { componentId = it } + StatusPicker("Dienst setzen auf", componentState, data.componentStates, emptyLabel = "nicht ändern") { componentState = it } + Button( + onClick = { + viewModel.statusAct( + mapOf( + "action" to "create_incident", "title" to title.trim(), "body" to body.trim(), "status" to level, + "componentId" to componentId.toIntOrNull(), "componentState" to componentState.ifBlank { null }, + ), + "Meldung veröffentlicht", + ) + title = ""; body = "" + }, + enabled = title.isNotBlank() && !state.loading, + modifier = Modifier.fillMaxWidth(), + ) { Text("Veröffentlichen") } + } + } + } + } + item { SectionTitle("Meldungen (letzte 14 Tage)") } + if (data.incidents.isEmpty()) item { Text("Keine Meldungen.", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) } + items(data.incidents, key = { it.id }) { i -> + var update by rememberSaveable(i.id) { mutableStateOf("") } + DeskCard { + Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text(i.title, Modifier.weight(1f), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold) + if (state.canManage) IconButton(onClick = { viewModel.statusAct(mapOf("action" to "delete_incident", "id" to i.id), "Meldung gelöscht") }) { + Icon(Icons.Filled.Close, contentDescription = "Meldung löschen") + } + } + Text(i.statusLabel + (i.component?.let { " · $it" } ?: "") + " · " + i.createdAt.take(16).replace('T', ' '), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + i.updates.forEach { u -> + Text(u.statusLabel + if (u.body.isNotBlank()) ": ${u.body}" else "", style = MaterialTheme.typography.bodySmall) + } + if (state.canManage && i.status != "resolved") { + var next by rememberSaveable(i.id) { mutableStateOf(i.status) } + StatusPicker("Neue Stufe", next, data.incidentStates) { if (it.isNotBlank()) next = it } + OutlinedTextField(value = update, onValueChange = { update = it }, label = { Text("Update für die Kunden") }, modifier = Modifier.fillMaxWidth()) + OutlinedButton( + onClick = { viewModel.statusAct(mapOf("action" to "update_incident", "id" to i.id, "status" to next, "body" to update.trim()), "Update veröffentlicht"); update = "" }, + enabled = !state.loading, + modifier = Modifier.fillMaxWidth(), + ) { Text("Update senden") } + } + } + } + } + } + } + } +} + +/** Kachel für die Übersicht: durchschnittliche Bewertung der letzten Tage. Nur sichtbar, wenn die Abfrage eingeschaltet ist oder es Antworten gibt. */ +@Composable +fun CsatCard(response: CsatResponse, modifier: Modifier = Modifier) { + val s = response.stats + DeskCard(modifier) { + Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) { + Text("Kundenzufriedenheit (${response.days} Tage)", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold) + if (s.sent == 0) { + Text("Noch keine Abfragen verschickt.", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) + } else { + Text( + (s.avg?.let { String.format(I18n.locale(), "%.1f", it) + " ★" } ?: "–"), + style = MaterialTheme.typography.headlineSmall, color = Color(0xFFF5B301), fontWeight = FontWeight.Bold, + ) + Text( + tr("${s.answered} Antworten von ${s.sent} Abfragen") + (s.rate?.let { " (${String.format(I18n.locale(), "%.0f", it)} %)" } ?: ""), + style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + s.dist.sortedByDescending { it.rating }.forEach { d -> + Row(verticalAlignment = Alignment.CenterVertically) { + Text("★".repeat(d.rating), Modifier.weight(1f), color = Color(0xFFF5B301), style = MaterialTheme.typography.bodySmall) + Text(d.count.toString(), style = MaterialTheme.typography.bodySmall) + } + } + if (s.byStaff.isNotEmpty()) { + Text( + tr("Nach Mitarbeiter:") + " " + s.byStaff.joinToString(" · ") { "${it.name} ${String.format(I18n.locale(), "%.1f", it.avg)} ★ (${it.count})" }, + style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + } +} diff --git a/mobile/src/main/res/drawable/ic_launcher_foreground.xml b/mobile/src/main/res/drawable/ic_launcher_foreground.xml index bc816d61..c59687b3 100644 --- a/mobile/src/main/res/drawable/ic_launcher_foreground.xml +++ b/mobile/src/main/res/drawable/ic_launcher_foreground.xml @@ -1,33 +1,17 @@ + - - - + - - - - - + android:fillType="evenOdd" + android:pathData="M24,38 H35 A16,16 0 0 1 35,70 H24 Z M32.5,46.5 H35 A7.5,7.5 0 0 1 35,61.5 H32.5 Z" /> + + android:pathData="M57,38 h8.5 v12.5 h10 v-12.5 h8.5 v32 h-8.5 v-12.5 h-10 v12.5 h-8.5 z" /> diff --git a/mobile/src/main/res/drawable/ic_launcher_monochrome.xml b/mobile/src/main/res/drawable/ic_launcher_monochrome.xml new file mode 100644 index 00000000..803a7084 --- /dev/null +++ b/mobile/src/main/res/drawable/ic_launcher_monochrome.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/mobile/src/main/res/layout/widget_deskhub.xml b/mobile/src/main/res/layout/widget_deskhub.xml index fa74b589..068c5440 100644 --- a/mobile/src/main/res/layout/widget_deskhub.xml +++ b/mobile/src/main/res/layout/widget_deskhub.xml @@ -48,7 +48,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="10dp" - android:text="Lade …" + android:text="@string/widget_loading" android:textColor="#FFFFFF" android:textSize="14sp" android:visibility="gone" /> @@ -82,7 +82,7 @@ @@ -108,7 +108,7 @@ @@ -135,7 +135,7 @@ @@ -161,7 +161,7 @@ diff --git a/mobile/src/main/res/layout/widget_deskhub_large.xml b/mobile/src/main/res/layout/widget_deskhub_large.xml index b5a348a1..480aad96 100644 --- a/mobile/src/main/res/layout/widget_deskhub_large.xml +++ b/mobile/src/main/res/layout/widget_deskhub_large.xml @@ -48,7 +48,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="10dp" - android:text="Lade …" + android:text="@string/widget_loading" android:textColor="#FFFFFF" android:textSize="14sp" android:visibility="gone" /> @@ -82,7 +82,7 @@ @@ -108,7 +108,7 @@ @@ -135,7 +135,7 @@ @@ -161,7 +161,7 @@ @@ -252,7 +252,7 @@ android:gravity="center" android:paddingTop="9dp" android:paddingBottom="9dp" - android:text="Alle Tickets" + android:text="@string/widget_all_tickets" android:textColor="#FFFFFF" android:textSize="13sp" /> @@ -273,7 +273,7 @@ android:maxLines="1" android:paddingTop="6dp" android:paddingBottom="6dp" - android:text="Verfügbar" + android:text="@string/widget_available" android:textColor="#FFFFFF" android:textSize="11sp" /> @@ -288,7 +288,7 @@ android:maxLines="1" android:paddingTop="6dp" android:paddingBottom="6dp" - android:text="Beschäftigt" + android:text="@string/widget_busy" android:textColor="#FFFFFF" android:textSize="11sp" /> @@ -303,7 +303,7 @@ android:maxLines="1" android:paddingTop="6dp" android:paddingBottom="6dp" - android:text="Pause" + android:text="@string/widget_break" android:textColor="#FFFFFF" android:textSize="11sp" /> @@ -318,7 +318,7 @@ android:maxLines="1" android:paddingTop="6dp" android:paddingBottom="6dp" - android:text="Offline" + android:text="@string/widget_offline" android:textColor="#FFFFFF" android:textSize="11sp" /> diff --git a/mobile/src/main/res/layout/widget_deskhub_small.xml b/mobile/src/main/res/layout/widget_deskhub_small.xml index 014d7341..6f98bcef 100644 --- a/mobile/src/main/res/layout/widget_deskhub_small.xml +++ b/mobile/src/main/res/layout/widget_deskhub_small.xml @@ -39,7 +39,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" - android:text="Lade …" + android:text="@string/widget_loading" android:textColor="#FFFFFF" android:textSize="13sp" android:visibility="gone" /> @@ -75,7 +75,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" - android:text="Meine" + android:text="@string/widget_mine" android:textColor="#E6FFFFFF" android:textSize="12sp" /> @@ -105,7 +105,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" - android:text="Überfällig" + android:text="@string/widget_overdue" android:textColor="#E6FFFFFF" android:textSize="12sp" /> diff --git a/mobile/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/mobile/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 6b78462d..8fde4563 100644 --- a/mobile/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/mobile/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -2,4 +2,5 @@ + diff --git a/mobile/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/mobile/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 6b78462d..8fde4563 100644 --- a/mobile/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/mobile/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -2,4 +2,5 @@ + diff --git a/mobile/src/main/res/values-en/strings.xml b/mobile/src/main/res/values-en/strings.xml new file mode 100644 index 00000000..d8da426c --- /dev/null +++ b/mobile/src/main/res/values-en/strings.xml @@ -0,0 +1,14 @@ + + Open, unassigned and overdue tickets, the most important tickets to tap, and your status. In three sizes. + My + Unassigned + Unanswered + Overdue + All tickets + Busy + Offline + Break + Available + Loading … + As of %1$s + diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml index 7bcfb114..d8231d57 100644 --- a/mobile/src/main/res/values/strings.xml +++ b/mobile/src/main/res/values/strings.xml @@ -1,5 +1,16 @@ - - deskhub - deskhub-Server - Offene, nicht zugewiesene und überfällige Tickets, die wichtigsten Tickets zum Antippen und dein Status. In drei Größen. - + + DeskHub + DeskHub-Server + Offene, nicht zugewiesene und überfällige Tickets, die wichtigsten Tickets zum Antippen und dein Status. In drei Größen. + Meine + Nicht zugew. + Unbeantw. + Überfällig + Alle Tickets + Beschäftigt + Offline + Pause + Verfügbar + Lade … + Stand %1$s +