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