full download (excel)
This commit is contained in:
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user