full download (excel)

This commit is contained in:
oxidiert
2025-09-12 10:48:55 +02:00
parent 67c11720b9
commit 93d2fa4333

View File

@ -20,7 +20,7 @@ class DatabaseButtonHandler(
private val activity: MainActivity, private val activity: MainActivity,
private val databaseButton: Button, private val databaseButton: Button,
private val onClose: () -> Unit, 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 languageIDProvider: () -> String = { "GERMAN" }
) { ) {
private val uiScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) private val uiScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
@ -84,15 +84,13 @@ class DatabaseButtonHandler(
uiScope.launch { uiScope.launch {
try { try {
progress.visibility = View.VISIBLE progress.visibility = View.VISIBLE
val exportFile = exportHeadersForAllClients()
val exportResult = exportHeadersForAllClients()
progress.visibility = View.GONE progress.visibility = View.GONE
if (exportResult != null) { if (exportFile != null) {
Toast.makeText( Toast.makeText(
activity, activity,
"Export erfolgreich: ${exportResult.absolutePath}", "Export erfolgreich: ${exportFile.absolutePath}",
Toast.LENGTH_LONG Toast.LENGTH_LONG
).show() ).show()
} else { } else {
@ -108,13 +106,9 @@ class DatabaseButtonHandler(
/** /**
* Erzeugt eine Excel-Datei (ClientHeaders.xlsx) mit: * Erzeugt eine Excel-Datei (ClientHeaders.xlsx) mit:
* - Spalten = IDs aus header_order.json (in genau der Reihenfolge) * - Zeile 1: Spalten-IDs (header_order.json)
* - Zeilen = je ein Client; Werte: * - Zeile 2: ENGLISCHE Fragen/Beschriftungen zu jeder ID (rein EN, keine IDs mehr)
* - "client_code" => code * - Ab Zeile 3: pro Client die Werte ("Done"/"Not Done"/Antwort oder "None")
* - Fragebogen-IDs (questionnaire_*) => "Done" / "Not Done"
* - alle anderen IDs => Antwort oder "None"
*
* Rückgabewert: Ausgabedatei oder null bei Fehler.
*/ */
private suspend fun exportHeadersForAllClients(): File? { private suspend fun exportHeadersForAllClients(): File? {
val orderedIds = loadOrderedIds() val orderedIds = loadOrderedIds()
@ -127,16 +121,27 @@ class DatabaseButtonHandler(
val wb = XSSFWorkbook() val wb = XSSFWorkbook()
val sheet = wb.createSheet("Headers") 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) for (i in 1..orderedIds.size) sheet.setColumnWidth(i, 36 * 256)
// Zeile 1: IDs
var col = 0 var col = 0
val header = sheet.createRow(0) val headerRow: Row = sheet.createRow(0)
header.createCell(col++).setCellValue("#") headerRow.createCell(col++).setCellValue("#")
orderedIds.forEach { id -> header.createCell(col++).setCellValue(id) } 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 -> clients.forEachIndexed { rowIdx, client ->
val row = sheet.createRow(rowIdx + 1) val row: Row = sheet.createRow(rowIdx + 2)
var c = 0 var c = 0
row.createCell(c++).setCellValue((rowIdx + 1).toDouble()) row.createCell(c++).setCellValue((rowIdx + 1).toDouble())
@ -146,25 +151,24 @@ class DatabaseButtonHandler(
val answerMap = answers.associate { it.questionId to it.answerValue } val answerMap = answers.associate { it.questionId to it.answerValue }
orderedIds.forEach { id -> orderedIds.forEach { id ->
// Rohwert wie bisher bestimmen
val raw = when { val raw = when {
id == "client_code" -> client.clientCode id == "client_code" -> client.clientCode
id in questionnaireIdSet -> if (statusMap[id] == true) "Done" else "Not Done" id in questionnaireIdSet -> if (statusMap[id] == true) "Done" else "Not Done"
else -> answerMap[id]?.takeIf { it.isNotBlank() } ?: "None" 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) val out = localizeForExportEn(id, raw)
row.createCell(c++).setCellValue(out) row.createCell(c++).setCellValue(out)
} }
} }
// Datei schreiben: extern + intern
val bytes = java.io.ByteArrayOutputStream().use { bos -> val bytes = java.io.ByteArrayOutputStream().use { bos ->
wb.write(bos) wb.write(bos); bos.toByteArray()
bos.toByteArray()
} }
wb.close() 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 extFile = extDir?.let { File(it, "ClientHeaders.xlsx") }?.apply { writeBytes(bytes) }
val intDir = File(activity.filesDir, "exports").apply { mkdirs() } val intDir = File(activity.filesDir, "exports").apply { mkdirs() }
@ -179,6 +183,48 @@ class DatabaseButtonHandler(
return extFile ?: intFile 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>): 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 // SCREEN 2: Fragebogen-Übersicht + "header"-Liste für einen Client
// --------------------------- // ---------------------------
@ -230,7 +276,7 @@ class DatabaseButtonHandler(
val questionnaireIdSet = allQuestionnaires.map { it.id }.toSet() val questionnaireIdSet = allQuestionnaires.map { it.id }.toSet()
val answerMap = allAnswersForClient.associate { it.questionId to it.answerValue } 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 allQuestionnaires
.sortedWith(compareBy({ extractQuestionnaireNumber(it.id) ?: Int.MAX_VALUE }, { it.id })) .sortedWith(compareBy({ extractQuestionnaireNumber(it.id) ?: Int.MAX_VALUE }, { it.id }))
.forEachIndexed { idx, q -> .forEachIndexed { idx, q ->
@ -256,13 +302,12 @@ class DatabaseButtonHandler(
} }
} }
// Tabelle 2: "header"-Liste aus header_order.json // Tabelle 2 (Header-Liste)
val orderedIds = loadOrderedIds() val orderedIds = loadOrderedIds()
orderedIds.forEachIndexed { idx, id -> orderedIds.forEachIndexed { idx, id ->
var rowBgColor: Int? = null var rowBgColor: Int? = null
val darkGray = 0xFFBDBDBD.toInt() val darkGray = 0xFFBDBDBD.toInt()
// 1) Rohwert ermitteln (für Logik & Farben)
val raw: String val raw: String
val bgColorForCells: Int? val bgColorForCells: Int?
@ -278,10 +323,8 @@ class DatabaseButtonHandler(
if (raw == "None") rowBgColor = darkGray if (raw == "None") rowBgColor = darkGray
} }
// 2) Anzeige-Wert über LanguageManager lokalisieren (nur Header/Wert-Spalte)
val display = localizeHeaderValue(id, raw) val display = localizeHeaderValue(id, raw)
// Für Fragebögen im Header: "#"(0), ID(1) und Wert(2) farbig HINTERLEGEN
val cellBg = if (bgColorForCells != null) val cellBg = if (bgColorForCells != null)
mapOf(0 to bgColorForCells, 1 to bgColorForCells, 2 to bgColorForCells) mapOf(0 to bgColorForCells, 1 to bgColorForCells, 2 to bgColorForCells)
else emptyMap() else emptyMap()
@ -289,9 +332,9 @@ class DatabaseButtonHandler(
addRow( addRow(
table = tableOrdered, table = tableOrdered,
cells = listOf((idx + 1).toString(), id, display), cells = listOf((idx + 1).toString(), id, display),
colorOverrides = emptyMap(), // keine Textfarben im Header colorOverrides = emptyMap(),
rowBgColor = rowBgColor, // dunkelgrau für "None" rowBgColor = rowBgColor,
cellBgOverrides = cellBg // GRÜN/ROT inkl. Spalte "#" cellBgOverrides = cellBg
) )
} }
} }
@ -364,55 +407,65 @@ class DatabaseButtonHandler(
} }
// --------------------------- // ---------------------------
// Hilfsfunktionen // Hilfsfunktionen (Lokalisierung & UI)
// --------------------------- // ---------------------------
private fun extractQuestionnaireNumber(id: String): Int? { private fun extractQuestionnaireNumber(id: String): Int? {
val m = Regex("^questionnaire_(\\d+)").find(id.lowercase()) val m = Regex("^questionnaire_(\\d+)").find(id.lowercase())
return m?.groupValues?.get(1)?.toIntOrNull() return m?.groupValues?.get(1)?.toIntOrNull()
} }
// Lokalisiert den im Header angezeigten Wert (Wert-Spalte) über LanguageManager. // Anzeige im Header (App) aktuelle UI-Sprache
// Probiert mehrere Schlüssel-Varianten, fällt ansonsten auf den Rohwert zurück.
// Ersetzt die alte Version 1:1
private fun localizeHeaderValue(id: String, raw: String): String { private fun localizeHeaderValue(id: String, raw: String): String {
// client_code nie übersetzen
if (id == "client_code") return raw if (id == "client_code") return raw
val lang = try { languageIDProvider() } catch (_: Exception) { "GERMAN" } val lang = try { languageIDProvider() } catch (_: Exception) { "GERMAN" }
fun stripBrackets(s: String): String { fun strip(s: String): String {
val m = Regex("^\\[(.*)]$").matchEntire(s.trim()) val m = Regex("^\\[(.*)]$").matchEntire(s.trim())
return m?.groupValues?.get(1) ?: s return m?.groupValues?.get(1) ?: s
} }
fun tryKey(key: String): String? { fun tryKey(key: String): String? {
val t = try { LanguageManager.getText(lang, key) } catch (_: Exception) { key } val t = try { LanguageManager.getText(lang, key) } catch (_: Exception) { key }
val stripped = stripBrackets(t) val out = strip(t)
// Wenn Ergebnis leer, gleich dem Key (nach evtl. Klammern) oder nur ein Platzhalter war: keine gültige Übersetzung if (out.isBlank() || out.equals(key, ignoreCase = true)) return null
if (stripped.isBlank() || stripped.equals(key, ignoreCase = true)) return null return out
return stripped
} }
// Kandidaten-Schlüssel in sinnvoller Reihenfolge
val norm = raw.lowercase().replace(Regex("[^a-z0-9]+"), "_").trim('_') val norm = raw.lowercase().replace(Regex("[^a-z0-9]+"), "_").trim('_')
val candidates = buildList { val candidates = buildList {
when (raw) { when (raw) { "Done" -> add("done"); "Not Done" -> add("not_done"); "None" -> add("none") }
"Done" -> add("done")
"Not Done" -> add("not_done")
"None" -> add("none")
}
add(raw) add(raw)
if (norm.isNotBlank() && norm != raw) add(norm) if (norm.isNotBlank() && norm != raw) add(norm)
if (norm.isNotBlank()) { if (norm.isNotBlank()) { add("${id}_$norm"); add("${id}-$norm") }
add("${id}_$norm")
add("${id}-$norm")
}
} }
for (key in candidates) { for (key in candidates) tryKey(key)?.let { return it }
tryKey(key)?.let { return it } // nur echte Übersetzungen übernehmen 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<String>) { private fun addHeaderRow(table: TableLayout, labels: List<String>) {
@ -425,13 +478,12 @@ class DatabaseButtonHandler(
private fun addRow( private fun addRow(
table: TableLayout, table: TableLayout,
cells: List<String>, cells: List<String>,
colorOverrides: Map<Int, Int> = emptyMap(), // optional: Textfarben je Spalte colorOverrides: Map<Int, Int> = emptyMap(),
rowBgColor: Int? = null, // optional: ganze Zeile hinterlegen rowBgColor: Int? = null,
cellBgOverrides: Map<Int, Int> = emptyMap() // optional: einzelne Zellen hinterlegen cellBgOverrides: Map<Int, Int> = emptyMap()
) { ) {
val row = TableRow(activity) val row = TableRow(activity)
rowBgColor?.let { row.setBackgroundColor(it) } rowBgColor?.let { row.setBackgroundColor(it) }
cells.forEachIndexed { index, text -> cells.forEachIndexed { index, text ->
val tv = makeBodyCell(text, colorOverrides[index], cellBgOverrides[index]) val tv = makeBodyCell(text, colorOverrides[index], cellBgOverrides[index])
row.addView(tv) row.addView(tv)
@ -527,33 +579,4 @@ class DatabaseButtonHandler(
} }
return v 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
}
} }