worked on header, added language manager and output (only english)

This commit is contained in:
oxidiert
2025-09-12 10:29:38 +02:00
parent 304cabc0d7
commit 67c11720b9
4 changed files with 282 additions and 66 deletions

View File

@ -27,6 +27,24 @@ android {
)
}
}
// Apache POI bringt viele META-INF-Dateien mit hier aus dem APK ausschließen
packaging {
resources {
excludes += listOf(
"META-INF/DEPENDENCIES",
"META-INF/LICENSE",
"META-INF/LICENSE.txt",
"META-INF/NOTICE",
"META-INF/NOTICE.txt",
"META-INF/NOTICE.md",
"META-INF/LICENSE.md",
"META-INF/*.kotlin_module",
"META-INF/versions/**"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
@ -44,22 +62,31 @@ dependencies {
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
implementation("com.google.code.gson:gson:2.10.1")
implementation("androidx.room:room-runtime:$room_version")
kapt("androidx.room:room-compiler:$room_version")
implementation("androidx.room:room-ktx:$room_version")
implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
// SQLCipher
implementation ("net.zetetic:android-database-sqlcipher:4.5.3@aar")
implementation ("androidx.sqlite:sqlite:2.1.0")
implementation ("androidx.sqlite:sqlite-framework:2.1.0")
implementation("net.zetetic:android-database-sqlcipher:4.5.3@aar")
implementation("androidx.sqlite:sqlite:2.1.0")
implementation("androidx.sqlite:sqlite-framework:2.1.0")
// Server Upload
implementation("com.squareup.okhttp3:okhttp:4.12.0")
}
// ---- Excel-Export (Apache POI) ----
// Leichtgewichtige OOXML-Implementierung + Kernbibliothek
implementation("org.apache.poi:poi:5.2.5")
implementation("org.apache.poi:poi-ooxml:5.2.5")
implementation("org.apache.poi:poi-ooxml-lite:5.2.5") //für kleinere Schemas
}

View File

@ -1,5 +1,6 @@
package com.dano.test1
import android.os.Environment
import android.util.Log
import android.view.View
import android.widget.*
@ -8,13 +9,19 @@ import com.dano.test1.data.Question
import com.dano.test1.data.Questionnaire
import kotlinx.coroutines.*
import org.json.JSONArray
import java.io.File
import java.nio.charset.Charset
import kotlin.math.roundToInt
import org.apache.poi.ss.usermodel.Row
import org.apache.poi.xssf.usermodel.XSSFWorkbook
class DatabaseButtonHandler(
private val activity: MainActivity,
private val databaseButton: Button,
private val onClose: () -> Unit
private val onClose: () -> Unit,
// Liefert die aktuelle Sprache; Default: "GERMAN"
private val languageIDProvider: () -> String = { "GERMAN" }
) {
private val uiScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private val tag = "DatabaseButtonHandler"
@ -37,8 +44,10 @@ class DatabaseButtonHandler(
val progress: ProgressBar = requireView(R.id.progressBar, "progressBar")
val emptyView: TextView = requireView(R.id.emptyView, "emptyView")
val backButton: Button = requireView(R.id.backButton, "backButton")
val btnDownloadHeader: Button = requireView(R.id.btnDownloadHeader, "btnDownloadHeader")
backButton.setOnClickListener { onClose() }
btnDownloadHeader.setOnClickListener { onDownloadHeadersClicked(progress) }
progress.visibility = View.VISIBLE
emptyView.visibility = View.GONE
@ -68,6 +77,108 @@ class DatabaseButtonHandler(
}
}
// ---------------------------
// Export: Header aller Clients als Excel
// ---------------------------
private fun onDownloadHeadersClicked(progress: ProgressBar) {
uiScope.launch {
try {
progress.visibility = View.VISIBLE
val exportResult = exportHeadersForAllClients()
progress.visibility = View.GONE
if (exportResult != null) {
Toast.makeText(
activity,
"Export erfolgreich: ${exportResult.absolutePath}",
Toast.LENGTH_LONG
).show()
} else {
Toast.makeText(activity, "Export fehlgeschlagen.", Toast.LENGTH_LONG).show()
}
} catch (e: Exception) {
progress.visibility = View.GONE
Log.e(tag, "Download Header Fehler: ${e.message}", e)
Toast.makeText(activity, "Fehler: ${e.message}", Toast.LENGTH_LONG).show()
}
}
}
/**
* 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.
*/
private suspend fun exportHeadersForAllClients(): File? {
val orderedIds = loadOrderedIds()
if (orderedIds.isEmpty()) return null
val clients = MyApp.database.clientDao().getAllClients()
val questionnaires = MyApp.database.questionnaireDao().getAll()
val questionnaireIdSet = questionnaires.map { it.id }.toSet()
val wb = XSSFWorkbook()
val sheet = wb.createSheet("Headers")
sheet.setColumnWidth(0, 8 * 256)
for (i in 1..orderedIds.size) sheet.setColumnWidth(i, 36 * 256)
var col = 0
val header = sheet.createRow(0)
header.createCell(col++).setCellValue("#")
orderedIds.forEach { id -> header.createCell(col++).setCellValue(id) }
clients.forEachIndexed { rowIdx, client ->
val row = sheet.createRow(rowIdx + 1)
var c = 0
row.createCell(c++).setCellValue((rowIdx + 1).toDouble())
val completedForClient = MyApp.database.completedQuestionnaireDao().getAllForClient(client.clientCode)
val statusMap = completedForClient.associate { it.questionnaireId to it.isDone }
val answers = MyApp.database.answerDao().getAnswersForClient(client.clientCode)
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)
val out = localizeForExportEn(id, raw)
row.createCell(c++).setCellValue(out)
}
}
val bytes = java.io.ByteArrayOutputStream().use { bos ->
wb.write(bos)
bos.toByteArray()
}
wb.close()
val extDir = activity.getExternalFilesDir(android.os.Environment.DIRECTORY_DOCUMENTS)?.apply { mkdirs() }
val extFile = extDir?.let { File(it, "ClientHeaders.xlsx") }?.apply { writeBytes(bytes) }
val intDir = File(activity.filesDir, "exports").apply { mkdirs() }
val intFile = File(intDir, "ClientHeaders.xlsx").apply { writeBytes(bytes) }
Toast.makeText(
activity,
"Export:\n• intern: ${intFile.absolutePath}\n• extern: ${extFile?.absolutePath ?: "—"}",
Toast.LENGTH_LONG
).show()
return extFile ?: intFile
}
// ---------------------------
// SCREEN 2: Fragebogen-Übersicht + "header"-Liste für einen Client
// ---------------------------
@ -119,72 +230,68 @@ class DatabaseButtonHandler(
val questionnaireIdSet = allQuestionnaires.map { it.id }.toSet()
val answerMap = allAnswersForClient.associate { it.questionId to it.answerValue }
// *** Sortierung der FRAGEBÖGEN nach questionnaire_1..., _2..., _3..., _4..., _5..., _6... ***
val sortedQuestionnaires = allQuestionnaires.sortedWith(
compareBy(
{ extractQuestionnaireNumber(it.id) ?: Int.MAX_VALUE },
{ it.id }
)
)
// Tabelle 1: Fragebögen (nur ✓ klickbar) hier NUR Textfarbe (kein Hintergrund)
allQuestionnaires
.sortedWith(compareBy({ extractQuestionnaireNumber(it.id) ?: Int.MAX_VALUE }, { it.id }))
.forEachIndexed { idx, q ->
val isDone = statusMap[q.id] ?: false
val statusText = if (isDone) "" else ""
val statusTextColor = if (isDone) 0xFF4CAF50.toInt() else 0xFFF44336.toInt()
// Tabelle 1: Fragebögen (nur ✓ klickbar) HIER KEINE Hintergrundfarben, nur Textfarbe
sortedQuestionnaires.forEachIndexed { idx, q ->
val isDone = statusMap[q.id] ?: false
val statusText = if (isDone) "" else ""
val statusTextColor = if (isDone) 0xFF4CAF50.toInt() else 0xFFF44336.toInt()
if (isDone) {
addClickableRow(
table = tableQ,
cells = listOf((idx + 1).toString(), q.id, statusText),
onClick = { openQuestionnaireDetailScreen(clientCode, q.id) },
colorOverrides = mapOf(2 to statusTextColor), // nur Text einfärben
cellBgOverrides = emptyMap()
)
} else {
addDisabledRow(
table = tableQ,
cells = listOf((idx + 1).toString(), q.id, statusText),
colorOverrides = mapOf(2 to statusTextColor), // nur Text einfärben
cellBgOverrides = emptyMap()
)
if (isDone) {
addClickableRow(
table = tableQ,
cells = listOf((idx + 1).toString(), q.id, statusText),
onClick = { openQuestionnaireDetailScreen(clientCode, q.id) },
colorOverrides = mapOf(2 to statusTextColor),
cellBgOverrides = emptyMap()
)
} else {
addDisabledRow(
table = tableQ,
cells = listOf((idx + 1).toString(), q.id, statusText),
colorOverrides = mapOf(2 to statusTextColor),
cellBgOverrides = emptyMap()
)
}
}
}
// Tabelle 2: "header"-Liste in der Reihenfolge aus assets/header_order.json
// HIER: GRÜN/ROT als HINTERGRUND für Fragebögen + GELB für "None"-Zeilen
// Tabelle 2: "header"-Liste aus header_order.json
val orderedIds = loadOrderedIds()
orderedIds.forEachIndexed { idx, id ->
var rowBgColor: Int? = null
val darkGray = 0xFFBDBDBD.toInt()
val (value, bgColorForCells) = when {
id == "client_code" -> clientCode to null
id in questionnaireIdSet -> {
if (statusMap[id] == true) {
"Done" to 0xFF4CAF50.toInt() // GRÜN als Hintergrund
} else {
"Not Done" to 0xFFF44336.toInt() // ROT als Hintergrund
}
}
else -> {
val v = answerMap[id]
val out = if (!v.isNullOrBlank()) v else "None"
if (out == "None") {
rowBgColor = 0xFFFFF59D.toInt() // GELB ganze Zeile bei None
}
out to null
}
// 1) Rohwert ermitteln (für Logik & Farben)
val raw: String
val bgColorForCells: Int?
if (id == "client_code") {
raw = clientCode
bgColorForCells = null
} else if (id in questionnaireIdSet) {
raw = if (statusMap[id] == true) "Done" else "Not Done"
bgColorForCells = if (raw == "Done") 0xFF4CAF50.toInt() else 0xFFF44336.toInt()
} else {
raw = answerMap[id]?.takeIf { it.isNotBlank() } ?: "None"
bgColorForCells = null
if (raw == "None") rowBgColor = darkGray
}
// Für Fragebögen im Header: ID (Spalte 2) und Wert (Spalte 3) farbig HINTERLEGEN
val cellBg = if (bgColorForCells != null) mapOf(1 to bgColorForCells, 2 to bgColorForCells) else emptyMap()
// 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()
addRow(
table = tableOrdered,
cells = listOf((idx + 1).toString(), id, value),
cells = listOf((idx + 1).toString(), id, display),
colorOverrides = emptyMap(), // keine Textfarben im Header
rowBgColor = rowBgColor, // GELB für "None"
cellBgOverrides = cellBg // GRÜN/ROT für Done/Not Done
rowBgColor = rowBgColor, // dunkelgrau für "None"
cellBgOverrides = cellBg // GRÜN/ROT inkl. Spalte "#"
)
}
}
@ -260,11 +367,54 @@ class DatabaseButtonHandler(
// Hilfsfunktionen
// ---------------------------
private fun extractQuestionnaireNumber(id: String): Int? {
// Erwartet Präfix "questionnaire_<zahl>"
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
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 {
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
}
// 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")
}
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 } // nur echte Übersetzungen übernehmen
}
return raw // Fallback ohne eckige Klammern
}
private fun addHeaderRow(table: TableLayout, labels: List<String>) {
val row = TableRow(activity)
labels.forEach { label -> row.addView(makeHeaderCell(label)) }
@ -377,4 +527,33 @@ 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
}
}

View File

@ -345,13 +345,16 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
private fun setupDatabaseButtonHandler() {
DatabaseButtonHandler(
activity = activity,
databaseButton = databaseButton
) {
// zurück zum Opening-Screen
init()
}.setup()
databaseButton = databaseButton,
onClose = {
// zurück zum Opening-Screen
init()
},
languageIDProvider = { languageID } // aktuelle Sprache an DatabaseButtonHandler weitergeben
).setup()
}
private fun updateMainButtonsState(isDatabaseAvailable: Boolean) {
val buttons = listOf(buttonLoad, saveButton, editButton, databaseButton) // <-- NEU dabei
buttons.forEach { button ->

View File

@ -53,6 +53,13 @@
android:paddingTop="8dp"
android:paddingBottom="8dp" />
<!-- NEU: Download-Button -->
<Button
android:id="@+id/btnDownloadHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Download Header" />
<Button
android:id="@+id/backButton"
android:layout_width="match_parent"