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 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>): 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<String>) {
@ -425,13 +478,12 @@ class DatabaseButtonHandler(
private fun addRow(
table: TableLayout,
cells: List<String>,
colorOverrides: Map<Int, Int> = emptyMap(), // optional: Textfarben je Spalte
rowBgColor: Int? = null, // optional: ganze Zeile hinterlegen
cellBgOverrides: Map<Int, Int> = emptyMap() // optional: einzelne Zellen hinterlegen
colorOverrides: Map<Int, Int> = emptyMap(),
rowBgColor: Int? = null,
cellBgOverrides: Map<Int, Int> = 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
}
}