From 93d2fa43330e432f8ca9dec8dd5c5694b0a1de11 Mon Sep 17 00:00:00 2001 From: oxidiert Date: Fri, 12 Sep 2025 10:48:55 +0200 Subject: [PATCH] full download (excel) --- .../com/dano/test1/DatabaseButtonHandler.kt | 197 ++++++++++-------- 1 file changed, 110 insertions(+), 87 deletions(-) diff --git a/app/src/main/java/com/dano/test1/DatabaseButtonHandler.kt b/app/src/main/java/com/dano/test1/DatabaseButtonHandler.kt index 9c46954..7c30611 100644 --- a/app/src/main/java/com/dano/test1/DatabaseButtonHandler.kt +++ b/app/src/main/java/com/dano/test1/DatabaseButtonHandler.kt @@ -20,7 +20,7 @@ class DatabaseButtonHandler( private val activity: MainActivity, private val databaseButton: Button, private val onClose: () -> Unit, - // Liefert die aktuelle Sprache; Default: "GERMAN" + // Aktuelle UI-Sprache für die Bildschirm-Anzeige (header-Tabelle) private val languageIDProvider: () -> String = { "GERMAN" } ) { private val uiScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) @@ -84,15 +84,13 @@ class DatabaseButtonHandler( uiScope.launch { try { progress.visibility = View.VISIBLE - - val exportResult = exportHeadersForAllClients() - + val exportFile = exportHeadersForAllClients() progress.visibility = View.GONE - if (exportResult != null) { + if (exportFile != null) { Toast.makeText( activity, - "Export erfolgreich: ${exportResult.absolutePath}", + "Export erfolgreich: ${exportFile.absolutePath}", Toast.LENGTH_LONG ).show() } else { @@ -108,13 +106,9 @@ class DatabaseButtonHandler( /** * Erzeugt eine Excel-Datei (ClientHeaders.xlsx) mit: - * - Spalten = IDs aus header_order.json (in genau der Reihenfolge) - * - Zeilen = je ein Client; Werte: - * - "client_code" => code - * - Fragebogen-IDs (questionnaire_*) => "Done" / "Not Done" - * - alle anderen IDs => Antwort oder "None" - * - * Rückgabewert: Ausgabedatei oder null bei Fehler. + * - Zeile 1: Spalten-IDs (header_order.json) + * - Zeile 2: ENGLISCHE Fragen/Beschriftungen zu jeder ID (rein EN, keine IDs mehr) + * - Ab Zeile 3: pro Client die Werte ("Done"/"Not Done"/Antwort oder "None") */ private suspend fun exportHeadersForAllClients(): File? { val orderedIds = loadOrderedIds() @@ -127,16 +121,27 @@ class DatabaseButtonHandler( val wb = XSSFWorkbook() val sheet = wb.createSheet("Headers") - sheet.setColumnWidth(0, 8 * 256) + sheet.setColumnWidth(0, 8 * 256) // # for (i in 1..orderedIds.size) sheet.setColumnWidth(i, 36 * 256) + // Zeile 1: IDs var col = 0 - val header = sheet.createRow(0) - header.createCell(col++).setCellValue("#") - orderedIds.forEach { id -> header.createCell(col++).setCellValue(id) } + val headerRow: Row = sheet.createRow(0) + headerRow.createCell(col++).setCellValue("#") + orderedIds.forEach { id -> headerRow.createCell(col++).setCellValue(id) } + // Zeile 2: Fragen/Titel konsequent auf EN + val questionRow: Row = sheet.createRow(1) + var qc = 0 + questionRow.createCell(qc++).setCellValue("Question (EN)") + for (id in orderedIds) { + val englishQuestion = englishQuestionForId(id, questionnaireIdSet) + questionRow.createCell(qc++).setCellValue(englishQuestion) + } + + // Ab Zeile 3: Werte je Client clients.forEachIndexed { rowIdx, client -> - val row = sheet.createRow(rowIdx + 1) + val row: Row = sheet.createRow(rowIdx + 2) var c = 0 row.createCell(c++).setCellValue((rowIdx + 1).toDouble()) @@ -146,25 +151,24 @@ class DatabaseButtonHandler( val answerMap = answers.associate { it.questionId to it.answerValue } orderedIds.forEach { id -> - // Rohwert wie bisher bestimmen val raw = when { id == "client_code" -> client.clientCode id in questionnaireIdSet -> if (statusMap[id] == true) "Done" else "Not Done" else -> answerMap[id]?.takeIf { it.isNotBlank() } ?: "None" } - // NEU: für Excel auf Englisch lokalisieren (Done/Not Done/None bleiben) + // Für Export in EN lokalisieren; Done/Not Done/None bleiben unverändert. val out = localizeForExportEn(id, raw) row.createCell(c++).setCellValue(out) } } + // Datei schreiben: extern + intern val bytes = java.io.ByteArrayOutputStream().use { bos -> - wb.write(bos) - bos.toByteArray() + wb.write(bos); bos.toByteArray() } wb.close() - val extDir = activity.getExternalFilesDir(android.os.Environment.DIRECTORY_DOCUMENTS)?.apply { mkdirs() } + val extDir = activity.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)?.apply { mkdirs() } val extFile = extDir?.let { File(it, "ClientHeaders.xlsx") }?.apply { writeBytes(bytes) } val intDir = File(activity.filesDir, "exports").apply { mkdirs() } @@ -179,6 +183,48 @@ class DatabaseButtonHandler( return extFile ?: intFile } + // Liefert einen englischen, lesbaren Fragetext/Titel zu einer ID (nur EN, keine IDs). + private suspend fun englishQuestionForId(id: String, questionnaireIdSet: Set): String { + // 1) Spezielle Fälle + if (id == "client_code") return "Client code" + if (id in questionnaireIdSet && !id.contains('-')) return "Questionnaire status" + + // 2) Versuch: LanguageManager EN für die ID + localizeEnglishNoBrackets(id)?.let { lm -> + if (!looksLikeId(lm, id)) return lm + } + + // 3) Falls ID wie "...-field_name": nur den Feldteil humanisieren + val fieldPart = id.substringAfterLast('-', id) + localizeEnglishNoBrackets(fieldPart)?.let { lm -> + if (!looksLikeId(lm, fieldPart)) return lm + } + + // 4) Humanisierte englische Fallbacks aus der ID (Titel-Case, Unterstriche → Leerzeichen) + val pretty = humanizeIdToEnglish(fieldPart) + if (pretty.isNotBlank()) return pretty + + // 5) Letzter Fallback: „Question“ + return "Question" + } + + private fun looksLikeId(text: String, originalId: String): Boolean { + // sieht wie die ID aus (nahezu identisch oder nur Klammern/Formatierung) + val normText = text.lowercase().replace(Regex("[^a-z0-9]+"), "_").trim('_') + val normId = originalId.lowercase().replace(Regex("[^a-z0-9]+"), "_").trim('_') + return normText == normId + } + + private fun humanizeIdToEnglish(source: String): String { + val s = source + .replace(Regex("^questionnaire_\\d+_"), "") // Präfix entfernen + .replace('_', ' ') + .trim() + if (s.isBlank()) return s + // Title Case + return s.split(Regex("\\s+")).joinToString(" ") { it.lowercase().replaceFirstChar { c -> c.titlecase() } } + } + // --------------------------- // SCREEN 2: Fragebogen-Übersicht + "header"-Liste für einen Client // --------------------------- @@ -230,7 +276,7 @@ class DatabaseButtonHandler( val questionnaireIdSet = allQuestionnaires.map { it.id }.toSet() val answerMap = allAnswersForClient.associate { it.questionId to it.answerValue } - // Tabelle 1: Fragebögen (nur ✓ klickbar) – hier NUR Textfarbe (kein Hintergrund) + // Tabelle 1 (nur Statusfarbe als Textfarbe) allQuestionnaires .sortedWith(compareBy({ extractQuestionnaireNumber(it.id) ?: Int.MAX_VALUE }, { it.id })) .forEachIndexed { idx, q -> @@ -256,13 +302,12 @@ class DatabaseButtonHandler( } } - // Tabelle 2: "header"-Liste aus header_order.json + // Tabelle 2 (Header-Liste) val orderedIds = loadOrderedIds() orderedIds.forEachIndexed { idx, id -> var rowBgColor: Int? = null val darkGray = 0xFFBDBDBD.toInt() - // 1) Rohwert ermitteln (für Logik & Farben) val raw: String val bgColorForCells: Int? @@ -278,10 +323,8 @@ class DatabaseButtonHandler( if (raw == "None") rowBgColor = darkGray } - // 2) Anzeige-Wert über LanguageManager lokalisieren (nur Header/Wert-Spalte) val display = localizeHeaderValue(id, raw) - // Für Fragebögen im Header: "#"(0), ID(1) und Wert(2) farbig HINTERLEGEN val cellBg = if (bgColorForCells != null) mapOf(0 to bgColorForCells, 1 to bgColorForCells, 2 to bgColorForCells) else emptyMap() @@ -289,9 +332,9 @@ class DatabaseButtonHandler( addRow( table = tableOrdered, cells = listOf((idx + 1).toString(), id, display), - colorOverrides = emptyMap(), // keine Textfarben im Header - rowBgColor = rowBgColor, // dunkelgrau für "None" - cellBgOverrides = cellBg // GRÜN/ROT – inkl. Spalte "#" + colorOverrides = emptyMap(), + rowBgColor = rowBgColor, + cellBgOverrides = cellBg ) } } @@ -364,55 +407,65 @@ class DatabaseButtonHandler( } // --------------------------- - // Hilfsfunktionen + // Hilfsfunktionen (Lokalisierung & UI) // --------------------------- private fun extractQuestionnaireNumber(id: String): Int? { val m = Regex("^questionnaire_(\\d+)").find(id.lowercase()) return m?.groupValues?.get(1)?.toIntOrNull() } - // Lokalisiert den im Header angezeigten Wert (Wert-Spalte) über LanguageManager. - // Probiert mehrere Schlüssel-Varianten, fällt ansonsten auf den Rohwert zurück. - // Ersetzt die alte Version 1:1 + // Anzeige im Header (App) – aktuelle UI-Sprache private fun localizeHeaderValue(id: String, raw: String): String { - // client_code nie übersetzen if (id == "client_code") return raw val lang = try { languageIDProvider() } catch (_: Exception) { "GERMAN" } - fun stripBrackets(s: String): String { + fun strip(s: String): String { val m = Regex("^\\[(.*)]$").matchEntire(s.trim()) return m?.groupValues?.get(1) ?: s } fun tryKey(key: String): String? { val t = try { LanguageManager.getText(lang, key) } catch (_: Exception) { key } - val stripped = stripBrackets(t) - // Wenn Ergebnis leer, gleich dem Key (nach evtl. Klammern) oder nur ein Platzhalter war: keine gültige Übersetzung - if (stripped.isBlank() || stripped.equals(key, ignoreCase = true)) return null - return stripped + val out = strip(t) + if (out.isBlank() || out.equals(key, ignoreCase = true)) return null + return out } - // Kandidaten-Schlüssel in sinnvoller Reihenfolge val norm = raw.lowercase().replace(Regex("[^a-z0-9]+"), "_").trim('_') val candidates = buildList { - when (raw) { - "Done" -> add("done") - "Not Done" -> add("not_done") - "None" -> add("none") - } + when (raw) { "Done" -> add("done"); "Not Done" -> add("not_done"); "None" -> add("none") } add(raw) if (norm.isNotBlank() && norm != raw) add(norm) - if (norm.isNotBlank()) { - add("${id}_$norm") - add("${id}-$norm") - } + if (norm.isNotBlank()) { add("${id}_$norm"); add("${id}-$norm") } } - for (key in candidates) { - tryKey(key)?.let { return it } // nur echte Übersetzungen übernehmen + for (key in candidates) tryKey(key)?.let { return it } + return raw + } + + private fun localizeEnglishNoBrackets(key: String): String? { + val t = try { LanguageManager.getText("ENGLISH", key) } catch (_: Exception) { null } + if (t == null) return null + val m = Regex("^\\[(.*)]$").matchEntire(t.trim()) + val stripped = m?.groupValues?.get(1) ?: t + if (stripped.isBlank() || stripped.equals(key, ignoreCase = true)) return null + return stripped + } + + // Englisch für Export; belässt Done/Not Done/None. + private fun localizeForExportEn(id: String, raw: String): String { + if (id == "client_code") return raw + if (raw == "Done" || raw == "Not Done" || raw == "None") return raw + + val norm = raw.lowercase().replace(Regex("[^a-z0-9]+"), "_").trim('_') + val candidates = buildList { + add(raw) + if (norm.isNotBlank() && norm != raw) add(norm) + if (norm.isNotBlank()) { add("${id}_$norm"); add("${id}-$norm") } } - return raw // Fallback ohne eckige Klammern + for (key in candidates) localizeEnglishNoBrackets(key)?.let { return it } + return raw } private fun addHeaderRow(table: TableLayout, labels: List) { @@ -425,13 +478,12 @@ class DatabaseButtonHandler( private fun addRow( table: TableLayout, cells: List, - colorOverrides: Map = emptyMap(), // optional: Textfarben je Spalte - rowBgColor: Int? = null, // optional: ganze Zeile hinterlegen - cellBgOverrides: Map = emptyMap() // optional: einzelne Zellen hinterlegen + colorOverrides: Map = emptyMap(), + rowBgColor: Int? = null, + cellBgOverrides: Map = emptyMap() ) { val row = TableRow(activity) rowBgColor?.let { row.setBackgroundColor(it) } - cells.forEachIndexed { index, text -> val tv = makeBodyCell(text, colorOverrides[index], cellBgOverrides[index]) row.addView(tv) @@ -527,33 +579,4 @@ class DatabaseButtonHandler( } return v } - - private fun localizeForExportEn(id: String, raw: String): String { - if (id == "client_code") return raw - if (raw == "Done" || raw == "Not Done" || raw == "None") return raw - - fun stripBrackets(s: String): String { - val m = Regex("^\\[(.*)]$").matchEntire(s.trim()) - return m?.groupValues?.get(1) ?: s - } - fun tryKey(key: String): String? { - val t = try { LanguageManager.getText("ENGLISH", key) } catch (_: Exception) { key } - val stripped = stripBrackets(t) - if (stripped.isBlank() || stripped.equals(key, ignoreCase = true)) return null - return stripped - } - - val norm = raw.lowercase().replace(Regex("[^a-z0-9]+"), "_").trim('_') - val candidates = buildList { - add(raw) - if (norm.isNotBlank() && norm != raw) add(norm) - if (norm.isNotBlank()) { - add("${id}_$norm") - add("${id}-$norm") - } - } - for (key in candidates) tryKey(key)?.let { return it } - return raw - } - }