worked on header, added language manager and output (only english)
This commit is contained in:
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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 ->
|
||||
|
||||
@ -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"
|
||||
|
||||
Reference in New Issue
Block a user