diff --git a/app/src/main/java/com/dano/test1/DatabaseButtonHandler.kt b/app/src/main/java/com/dano/test1/DatabaseButtonHandler.kt index 93e5c38..4f56508 100644 --- a/app/src/main/java/com/dano/test1/DatabaseButtonHandler.kt +++ b/app/src/main/java/com/dano/test1/DatabaseButtonHandler.kt @@ -1,11 +1,5 @@ package com.dano.test1 -import android.content.ContentValues -import android.media.MediaScannerConnection -import android.net.Uri -import android.os.Build -import android.os.Environment -import android.provider.MediaStore import android.util.Log import android.view.View import android.widget.* @@ -13,25 +7,21 @@ import com.dano.test1.data.Client 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 +import org.json.JSONArray class DatabaseButtonHandler( private val activity: MainActivity, private val databaseButton: Button, private val onClose: () -> Unit, - // Aktuelle UI-Sprache für die Bildschirm-Anzeige (header-Tabelle) private val languageIDProvider: () -> String = { "GERMAN" } ) { private val uiScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) private val tag = "DatabaseButtonHandler" - // Cache für geladene IDs aus assets/header_order.json oder header_order.xlsx - private var orderedIdsCache: List? = null + // Services (wie bei dir) + private val headerRepo = HeaderOrderRepository(activity) + private val exporter = ExcelExportService(activity, headerRepo) fun setup() { databaseButton.text = "Datenbank" @@ -44,12 +34,21 @@ class DatabaseButtonHandler( private fun openDatabaseScreen() { activity.setContentView(R.layout.database_screen) + val lang = safeLang() // ✨ + + val titleTv: TextView = requireView(R.id.title, "title") // ✨ val table: TableLayout = requireView(R.id.tableClients, "tableClients") 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") + // ✨ statische Texte lokalisieren + titleTv.text = t(lang, "database_clients_title") ?: "Datenbank – Clients" + emptyView.text = t(lang, "no_clients_available") ?: "Keine Clients vorhanden." + backButton.text = t(lang, "previous") ?: "Zurück" + btnDownloadHeader.text = t(lang, "download_header") ?: "Download Header" + backButton.setOnClickListener { onClose() } btnDownloadHeader.setOnClickListener { onDownloadHeadersClicked(progress) } @@ -57,7 +56,8 @@ class DatabaseButtonHandler( emptyView.visibility = View.GONE table.removeAllViews() - addHeaderRow(table, listOf("#", "Client-Code")) + // ✨ Spaltenkopf lokalisieren + addHeaderRow(table, listOf("#", t(lang, "client_code") ?: "Client-Code")) uiScope.launch { val clients: List = withContext(Dispatchers.IO) { @@ -88,17 +88,18 @@ class DatabaseButtonHandler( uiScope.launch { try { progress.visibility = View.VISIBLE - val savedUri = exportHeadersForAllClients() // -> speichert NUR in Downloads + val savedUri = exporter.exportHeadersForAllClients() progress.visibility = View.GONE + val lang = safeLang() // ✨ if (savedUri != null) { Toast.makeText( activity, - "Export erfolgreich: Downloads/ClientHeaders.xlsx", + t(lang, "export_success_downloads") ?: "Export erfolgreich: Downloads/ClientHeaders.xlsx", Toast.LENGTH_LONG ).show() } else { - Toast.makeText(activity, "Export fehlgeschlagen.", Toast.LENGTH_LONG).show() + Toast.makeText(activity, t(lang, "export_failed") ?: "Export fehlgeschlagen.", Toast.LENGTH_LONG).show() } } catch (e: Exception) { progress.visibility = View.GONE @@ -108,165 +109,27 @@ class DatabaseButtonHandler( } } - /** - * Erzeugt eine Excel-Datei (ClientHeaders.xlsx) mit: - * - Zeile 1: Spalten-IDs (header_order.json / header_order.xlsx) - * - 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") - * - Speichert AUSSCHLIESSLICH in den öffentlichen Geräte-Ordner "Downloads" - */ - private suspend fun exportHeadersForAllClients(): Uri? { - 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) - - // Zeile 1: IDs - var col = 0 - 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: Row = sheet.createRow(rowIdx + 2) - 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 -> - 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" - } - val out = localizeForExportEn(id, raw) // Export immer EN - row.createCell(c++).setCellValue(out) - } - } - - // Bytes erzeugen - val bytes = java.io.ByteArrayOutputStream().use { bos -> - wb.write(bos); bos.toByteArray() - } - wb.close() - - // -> nur in Downloads speichern (Q+ via MediaStore, sonst Public Downloads) - return saveToDownloads( - filename = "ClientHeaders.xlsx", - mimeType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - bytes = bytes - ) - } - - /** Speichert Bytes nach "Downloads". */ - private fun saveToDownloads(filename: String, mimeType: String, bytes: ByteArray): Uri? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - // Android 10+ (Scoped Storage) - val resolver = activity.contentResolver - val values = ContentValues().apply { - put(MediaStore.MediaColumns.DISPLAY_NAME, filename) - put(MediaStore.MediaColumns.MIME_TYPE, mimeType) - put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) - // optional: put(MediaStore.MediaColumns.IS_PENDING, 1) - } - val collection = MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) - val uri = resolver.insert(collection, values) - if (uri != null) { - resolver.openOutputStream(uri)?.use { it.write(bytes) } ?: return null - // optional: values.clear(); values.put(MediaStore.MediaColumns.IS_PENDING, 0); resolver.update(uri, values, null, null) - uri - } else null - } else { - // Android 9 und älter – public Downloads (WRITE_EXTERNAL_STORAGE kann nötig sein) - val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) - if (!downloadsDir.exists()) downloadsDir.mkdirs() - val outFile = File(downloadsDir, filename) - outFile.writeBytes(bytes) - // im System sichtbar machen - MediaScannerConnection.scanFile( - activity, - arrayOf(outFile.absolutePath), - arrayOf(mimeType), - null - ) - Uri.fromFile(outFile) - } - } - - // Liefert einen englischen, lesbaren Fragetext/Titel zu einer ID (nur EN, keine IDs). - private suspend fun englishQuestionForId(id: String, questionnaireIdSet: Set): String { - if (id == "client_code") return "Client code" - if (id in questionnaireIdSet && !id.contains('-')) return "Questionnaire status" - - localizeEnglishNoBrackets(id)?.let { lm -> - if (!looksLikeId(lm, id)) return lm - } - - val fieldPart = id.substringAfterLast('-', id) - localizeEnglishNoBrackets(fieldPart)?.let { lm -> - if (!looksLikeId(lm, fieldPart)) return lm - } - - val pretty = humanizeIdToEnglish(fieldPart) - if (pretty.isNotBlank()) return pretty - return "Question" - } - - private fun looksLikeId(text: String, originalId: String): Boolean { - 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+_"), "") - .replace('_', ' ') - .trim() - if (s.isBlank()) return s - 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 // --------------------------- private fun openClientOverviewScreen(clientCode: String) { activity.setContentView(R.layout.client_overview_screen) + val lang = safeLang() // ✨ + val title: TextView = requireView(R.id.titleClientOverview, "titleClientOverview") val tableQ: TableLayout = requireView(R.id.tableQuestionnaires, "tableQuestionnaires") val progress: ProgressBar = requireView(R.id.progressBarClient, "progressBarClient") val emptyView: TextView = requireView(R.id.emptyViewClient, "emptyViewClient") val backButton: Button = requireView(R.id.backButtonClient, "backButtonClient") - // "header" Sektion val headerLabel: TextView = requireView(R.id.headerLabel, "headerLabel") val tableOrdered: TableLayout = requireView(R.id.tableOrdered, "tableOrdered") - title.text = "Client: $clientCode – Fragebögen" - headerLabel.text = "header" + // ✨ + title.text = "${t(lang, "client") ?: "Client"}: $clientCode – ${t(lang, "questionnaires") ?: "Fragebögen"}" + headerLabel.text = t(lang, "headers") ?: "header" + backButton.text = t(lang, "previous") ?: "Zurück" backButton.setOnClickListener { openDatabaseScreen() } progress.visibility = View.VISIBLE @@ -274,8 +137,15 @@ class DatabaseButtonHandler( tableQ.removeAllViews() tableOrdered.removeAllViews() - addHeaderRow(tableQ, listOf("#", "Fragebogen-ID", "Status")) - addHeaderRow(tableOrdered, listOf("#", "ID", "Wert")) + // ✨ Tabellenköpfe lokalisieren + addHeaderRow( + tableQ, + listOf("#", t(lang, "questionnaire_id") ?: "Fragebogen-ID", t(lang, "status") ?: "Status") + ) + addHeaderRow( + tableOrdered, + listOf("#", t(lang, "id") ?: "ID", t(lang, "value") ?: "Wert") + ) uiScope.launch { val result = withContext(Dispatchers.IO) { @@ -292,7 +162,7 @@ class DatabaseButtonHandler( val allAnswersForClient = result.third if (allQuestionnaires.isEmpty()) { - emptyView.text = "Keine Fragebögen vorhanden." + emptyView.text = t(lang, "no_questionnaires") ?: "Keine Fragebögen vorhanden." emptyView.visibility = View.VISIBLE } @@ -300,7 +170,7 @@ class DatabaseButtonHandler( val questionnaireIdSet = allQuestionnaires.map { it.id }.toSet() val answerMap = allAnswersForClient.associate { it.questionId to it.answerValue } - // Tabelle 1 (nur Statusfarbe als Textfarbe) + // Tabelle 1 (Status) allQuestionnaires .sortedWith(compareBy({ extractQuestionnaireNumber(it.id) ?: Int.MAX_VALUE }, { it.id })) .forEachIndexed { idx, q -> @@ -313,52 +183,44 @@ class DatabaseButtonHandler( table = tableQ, cells = listOf((idx + 1).toString(), q.id, statusText), onClick = { openQuestionnaireDetailScreen(clientCode, q.id) }, - colorOverrides = mapOf(2 to statusTextColor), - cellBgOverrides = emptyMap() + colorOverrides = mapOf(2 to statusTextColor) ) } else { addDisabledRow( table = tableQ, cells = listOf((idx + 1).toString(), q.id, statusText), - colorOverrides = mapOf(2 to statusTextColor), - cellBgOverrides = emptyMap() + colorOverrides = mapOf(2 to statusTextColor) ) } } - // Farben (Material-like) - val lightGreen = 0xFFC8E6C9.toInt() // beantwortet (nicht-Fragebogen) - val lightRed = 0xFFFFCDD2.toInt() // unbeantwortet (nicht-Fragebogen) - val doneGreen = 0xFF4CAF50.toInt() // Fragebogen: Done (BLEIBT) - val notRed = 0xFFF44336.toInt() // Fragebogen: Not Done (BLEIBT) + // Farben + val lightGreen = 0xFFC8E6C9.toInt() + val lightRed = 0xFFFFCDD2.toInt() + val doneGreen = 0xFF4CAF50.toInt() + val notRed = 0xFFF44336.toInt() // Tabelle 2 (Header-Liste) - val orderedIds = loadOrderedIds() + val orderedIds = headerRepo.loadOrderedIds() orderedIds.forEachIndexed { idx, id -> var rowBgColor: Int? = null - val raw: String val cellBgForQuestionnaire: Int? if (id == "client_code") { - // client_code bleibt unmarkiert raw = clientCode cellBgForQuestionnaire = null } else if (id in questionnaireIdSet) { - // Fragebogenstatus: wie bisher grün/rot NUR für #, ID, Wert raw = if (statusMap[id] == true) "Done" else "Not Done" cellBgForQuestionnaire = if (raw == "Done") doneGreen else notRed } else { - // Normale Frage: raw = answerMap[id]?.takeIf { it.isNotBlank() } ?: "None" cellBgForQuestionnaire = null - // NEU: hellgrün wenn beantwortet, hellrot wenn None rowBgColor = if (raw == "None") lightRed else lightGreen } - val display = localizeHeaderValue(id, raw) + val display = localizeHeaderValue(id, raw, lang) // ✨ - // Für Fragebögen: "#"(0), ID(1), Wert(2) farbig hinterlegen – wie gehabt val cellBgOverrides = if (cellBgForQuestionnaire != null) mapOf(0 to cellBgForQuestionnaire, 1 to cellBgForQuestionnaire, 2 to cellBgForQuestionnaire) @@ -367,34 +229,39 @@ class DatabaseButtonHandler( addRow( table = tableOrdered, cells = listOf((idx + 1).toString(), id, display), - colorOverrides = emptyMap(), - rowBgColor = rowBgColor, // greift für normale Fragen (ganze Zeile inkl. #) - cellBgOverrides = cellBgOverrides // greift für Fragebögen (nur 3 Zellen) + rowBgColor = rowBgColor, + cellBgOverrides = cellBgOverrides ) } } } // --------------------------- - // SCREEN 3: Fragen & Antworten zu einem Fragebogen + // SCREEN 3: Fragen & Antworten eines Fragebogens // --------------------------- private fun openQuestionnaireDetailScreen(clientCode: String, questionnaireId: String) { activity.setContentView(R.layout.questionnaire_detail_screen) + val lang = safeLang() // ✨ + val title: TextView = requireView(R.id.titleQuestionnaireDetail, "titleQuestionnaireDetail") val table: TableLayout = requireView(R.id.tableQA, "tableQA") val progress: ProgressBar = requireView(R.id.progressBarQA, "progressBarQA") val emptyView: TextView = requireView(R.id.emptyViewQA, "emptyViewQA") val backButton: Button = requireView(R.id.backButtonQA, "backButtonQA") - title.text = "Client: $clientCode – Fragebogen: $questionnaireId" + // ✨ Titel + Back + Empty übersetzen + title.text = "${t(lang, "client") ?: "Client"}: $clientCode – ${t(lang, "questionnaire") ?: "Fragebogen"}: $questionnaireId" + backButton.text = t(lang, "previous") ?: "Zurück" + emptyView.text = t(lang, "no_questions_available") ?: "Keine Fragen vorhanden." backButton.setOnClickListener { openClientOverviewScreen(clientCode) } progress.visibility = View.VISIBLE emptyView.visibility = View.GONE table.removeAllViews() - addHeaderRow(table, listOf("#", "Frage", "Antwort")) + // ✨ Kopf: # | Frage | Antwort + addHeaderRow(table, listOf("#", t(lang, "question") ?: "Frage", t(lang, "answer") ?: "Antwort")) uiScope.launch { val (questions, answersForClient) = withContext(Dispatchers.IO) { @@ -407,7 +274,6 @@ class DatabaseButtonHandler( progress.visibility = View.GONE if (questions.isEmpty()) { - emptyView.text = "Keine Fragen vorhanden." emptyView.visibility = View.VISIBLE return@launch } @@ -415,148 +281,68 @@ class DatabaseButtonHandler( val answerMap = answersForClient.associate { it.questionId to it.answerValue } questions.forEachIndexed { idx, q: Question -> - val qText = q.question.takeIf { it.isNotBlank() } ?: q.questionId - val aText = answerMap[q.questionId]?.takeIf { it.isNotBlank() } ?: "—" + val baseId = q.questionId.substringAfterLast('-', q.questionId) // z.B. consent_signed, gender_male + val qText = localizeQuestionLabel(q.questionId, q.question, lang) // ✨ + val raw = answerMap[q.questionId]?.takeIf { it.isNotBlank() } ?: "—" + val aText = localizeAnswerValue(baseId, raw, lang) // ✨ addRow(table, listOf((idx + 1).toString(), qText, aText)) } } } // --------------------------- - // ordered_ids aus XLSX/JSON laden (mit Cache) + // Lokalisierungshilfen (Anzeige) // --------------------------- - private fun loadOrderedIds(): List { - orderedIdsCache?.let { return it } - val fromXlsx = runCatching { loadOrderedIdsFromExcel("header_order.xlsx") } - .onFailure { e -> Log.w(tag, "header_order.xlsx konnte nicht gelesen werden: ${e.message}") } - .getOrElse { emptyList() } + private fun safeLang(): String = try { languageIDProvider() } catch (_: Exception) { "GERMAN" } - if (fromXlsx.isNotEmpty()) { - orderedIdsCache = fromXlsx - return fromXlsx - } - - return try { - val stream = activity.assets.open("header_order.json") - val json = stream.readBytes().toString(Charset.forName("UTF-8")) - val arr = JSONArray(json) - val list = MutableList(arr.length()) { i -> arr.getString(i) } - orderedIdsCache = list - list - } catch (e: Exception) { - Log.e(tag, "Weder header_order.xlsx noch header_order.json verfügbar/gültig: ${e.message}") - Toast.makeText(activity, "Keine Header-Vorlage gefunden", Toast.LENGTH_LONG).show() - emptyList() - } + private fun stripBrackets(s: String): String { + val m = Regex("^\\[(.*)]$").matchEntire(s.trim()) + return m?.groupValues?.get(1) ?: s } - private fun loadOrderedIdsFromExcel(assetFileName: String): List { - activity.assets.open(assetFileName).use { input -> - XSSFWorkbook(input).use { wb -> - val sheet = wb.getSheetAt(0) ?: return emptyList() - val row = sheet.getRow(0) ?: return emptyList() - - val first = row.firstCellNum.toInt() - val last = row.lastCellNum.toInt() // exklusiv - val out = mutableListOf() - - for (i in first until last) { - val cell = row.getCell(i) ?: continue - val value = getCellAsString(cell).trim() - if (value.isEmpty()) continue - if (i == first && value == "#") continue // „#“ in Spalte 0 ignorieren - out.add(value) - } - return out - } - } + private fun t(lang: String, key: String): String? { + val txt = try { LanguageManager.getText(lang, key) } catch (_: Exception) { null } ?: return null + val out = stripBrackets(txt).trim() + return if (out.equals(key, true) || out.isBlank()) null else out } - private fun getCellAsString(cell: org.apache.poi.ss.usermodel.Cell): String = - when (cell.cellType) { - org.apache.poi.ss.usermodel.CellType.STRING -> cell.stringCellValue - org.apache.poi.ss.usermodel.CellType.NUMERIC -> - if (org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(cell)) - cell.dateCellValue.time.toString() - else { - val n = cell.numericCellValue - if (n % 1.0 == 0.0) n.toLong().toString() else n.toString() - } - org.apache.poi.ss.usermodel.CellType.BOOLEAN -> cell.booleanCellValue.toString() - org.apache.poi.ss.usermodel.CellType.FORMULA -> cell.richStringCellValue.string - else -> "" - } - - // --------------------------- - // Hilfsfunktionen (Lokalisierung & UI) - // --------------------------- - private fun extractQuestionnaireNumber(id: String): Int? { - val m = Regex("^questionnaire_(\\d+)").find(id.lowercase()) - return m?.groupValues?.get(1)?.toIntOrNull() + // ✨ Frage-Label für die UI + private fun localizeQuestionLabel(questionId: String, fallbackQuestionText: String?, lang: String): String { + val field = questionId.substringAfterLast('-', questionId) + t(lang, field)?.let { return it } // z.B. consent_instruction + t(lang, questionId)?.let { return it } // kompletter key + fallbackQuestionText?.takeIf { it.isNotBlank() }?.let { return it } + return field.replace('_', ' ').replaceFirstChar { it.titlecase() } } - // Anzeige im Header (App) – aktuelle UI-Sprache - private fun localizeHeaderValue(id: String, raw: String): String { - if (id == "client_code") return raw - - val lang = try { languageIDProvider() } catch (_: Exception) { "GERMAN" } - - 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 out = strip(t) - if (out.isBlank() || out.equals(key, ignoreCase = true)) return null - return out - } + // ✨ Antwort-Wert für die UI + private fun localizeAnswerValue(fieldId: String, raw: String, lang: String): String { + // Zahlen/Datum/Strich beibehalten + if (raw == "—") return raw + if (raw.matches(Regex("^\\d{1,4}([./-]\\d{1,2}([./-]\\d{1,4})?)?\$"))) return raw + // 1) direkter Key + t(lang, raw)?.let { return it } + // 2) normalisiert 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") } + if (norm.isNotBlank()) t(lang, norm)?.let { return it } + // 3) kombiniere Feld + Wert (häufiges Muster) + if (norm.isNotBlank()) { + t(lang, "${fieldId}_$norm")?.let { return it } + t(lang, "${fieldId}-${norm}")?.let { return it } } - - 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 } - val m = Regex("^\\[(.*)]$").matchEntire(t?.trim() ?: "") - val stripped = m?.groupValues?.get(1) ?: t - if (stripped == null || 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") } - } - for (key in candidates) localizeEnglishNoBrackets(key)?.let { return it } - return raw + // Klammern entfernen, falls aus LanguageManager so geliefert + return stripBrackets(raw) } // --------------------------- - // UI-Helfer + // Anzeige-Helfer // --------------------------- private fun addHeaderRow(table: TableLayout, labels: List) { val row = TableRow(activity) labels.forEach { label -> row.addView(makeHeaderCell(label)) } - table.addView(row) - addDivider(table) + table.addView(row); addDivider(table) } private fun addRow( @@ -569,11 +355,9 @@ class DatabaseButtonHandler( val row = TableRow(activity) rowBgColor?.let { row.setBackgroundColor(it) } cells.forEachIndexed { index, text -> - val tv = makeBodyCell(text, colorOverrides[index], cellBgOverrides[index]) - row.addView(tv) + val tv = makeBodyCell(text, colorOverrides[index], cellBgOverrides[index]); row.addView(tv) } - table.addView(row) - addDivider(table) + table.addView(row); addDivider(table) } private fun addClickableRow( @@ -590,11 +374,9 @@ class DatabaseButtonHandler( setOnClickListener { onClick() } } cells.forEachIndexed { index, text -> - val tv = makeBodyCell(text, colorOverrides[index], cellBgOverrides[index]) - row.addView(tv) + val tv = makeBodyCell(text, colorOverrides[index], cellBgOverrides[index]); row.addView(tv) } - table.addView(row) - addDivider(table) + table.addView(row); addDivider(table) } private fun addDisabledRow( @@ -603,25 +385,16 @@ class DatabaseButtonHandler( colorOverrides: Map = emptyMap(), cellBgOverrides: Map = emptyMap() ) { - val row = TableRow(activity).apply { - isClickable = false - isEnabled = false - alpha = 0.6f - } + val row = TableRow(activity).apply { isClickable = false; isEnabled = false; alpha = 0.6f } cells.forEachIndexed { index, text -> - val tv = makeBodyCell(text, colorOverrides[index], cellBgOverrides[index]) - row.addView(tv) + val tv = makeBodyCell(text, colorOverrides[index], cellBgOverrides[index]); row.addView(tv) } - table.addView(row) - addDivider(table) + table.addView(row); addDivider(table) } private fun addDivider(table: TableLayout) { val divider = View(activity) - val params = TableLayout.LayoutParams( - TableLayout.LayoutParams.MATCH_PARENT, - dp(1) - ) + val params = TableLayout.LayoutParams(TableLayout.LayoutParams.MATCH_PARENT, dp(1)) divider.layoutParams = params divider.setBackgroundColor(0xFFDDDDDD.toInt()) table.addView(divider) @@ -663,4 +436,25 @@ class DatabaseButtonHandler( } return v } + + private fun extractQuestionnaireNumber(id: String): Int? { + val m = Regex("^questionnaire_(\\d+)").find(id.lowercase()) + return m?.groupValues?.get(1)?.toIntOrNull() + } + + // ✨ alte localizeHeaderValue erweitert: nimmt jetzt lang mit + private fun localizeHeaderValue(id: String, raw: String, lang: String): String { + if (id == "client_code") return raw + + fun norm(s: String) = s.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) + val n = norm(raw) + if (n.isNotBlank() && n != raw) add(n) + if (n.isNotBlank()) { add("${id}_$n"); add("${id}-$n") } + } + for (k in candidates) t(lang, k)?.let { return it } + return stripBrackets(raw) + } } diff --git a/app/src/main/java/com/dano/test1/ExcelExportService.kt b/app/src/main/java/com/dano/test1/ExcelExportService.kt new file mode 100644 index 0000000..523fdca --- /dev/null +++ b/app/src/main/java/com/dano/test1/ExcelExportService.kt @@ -0,0 +1,163 @@ +package com.dano.test1 + +import android.content.ContentValues +import android.content.Context +import android.media.MediaScannerConnection +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.MediaStore +import org.apache.poi.ss.usermodel.Row +import org.apache.poi.xssf.usermodel.XSSFWorkbook + +class ExcelExportService( + private val context: Context, + private val headerRepo: HeaderOrderRepository +) { + + /** Baut die Excel-Datei und speichert sie ausschließlich unter "Downloads". */ + suspend fun exportHeadersForAllClients(): Uri? { + val orderedIds = headerRepo.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) + + // Row 1: IDs + var col = 0 + val headerRow: Row = sheet.createRow(0) + headerRow.createCell(col++).setCellValue("#") + orderedIds.forEach { id -> headerRow.createCell(col++).setCellValue(id) } + + // Row 2: Questions (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) + } + + // Rows 3+: Values per client + clients.forEachIndexed { rowIdx, client -> + val row: Row = sheet.createRow(rowIdx + 2) + 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 -> + 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" + } + val out = localizeForExportEn(id, raw) + row.createCell(c++).setCellValue(out) + } + } + + val bytes = java.io.ByteArrayOutputStream().use { bos -> + wb.write(bos); bos.toByteArray() + } + wb.close() + + return saveToDownloads( + filename = "ClientHeaders.xlsx", + mimeType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + bytes = bytes + ) + } + + /** Speichert Bytes nach "Downloads". */ + private fun saveToDownloads(filename: String, mimeType: String, bytes: ByteArray): Uri? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val resolver = context.contentResolver + val values = ContentValues().apply { + put(MediaStore.MediaColumns.DISPLAY_NAME, filename) + put(MediaStore.MediaColumns.MIME_TYPE, mimeType) + put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) + } + val collection = MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) + val uri = resolver.insert(collection, values) + if (uri != null) { + resolver.openOutputStream(uri)?.use { it.write(bytes) } ?: return null + uri + } else null + } else { + val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + if (!downloadsDir.exists()) downloadsDir.mkdirs() + val outFile = java.io.File(downloadsDir, filename) + outFile.writeBytes(bytes) + MediaScannerConnection.scanFile( + context, + arrayOf(outFile.absolutePath), + arrayOf(mimeType), + null + ) + Uri.fromFile(outFile) + } + } + + // ---------- Export-spezifische Lokalisierung (EN) ---------- + private suspend fun englishQuestionForId(id: String, questionnaireIdSet: Set): String { + if (id == "client_code") return "Client code" + if (id in questionnaireIdSet && !id.contains('-')) return "Questionnaire status" + + localizeEnglishNoBrackets(id)?.let { lm -> + if (!looksLikeId(lm, id)) return lm + } + + val fieldPart = id.substringAfterLast('-', id) + localizeEnglishNoBrackets(fieldPart)?.let { lm -> + if (!looksLikeId(lm, fieldPart)) return lm + } + + val pretty = humanizeIdToEnglish(fieldPart) + if (pretty.isNotBlank()) return pretty + return "Question" + } + + private fun looksLikeId(text: String, originalId: String): Boolean { + 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+_"), "").replace('_', ' ').trim() + if (s.isBlank()) return s + return s.split(Regex("\\s+")).joinToString(" ") { it.lowercase().replaceFirstChar { c -> c.titlecase() } } + } + + private fun localizeEnglishNoBrackets(key: String): String? { + val t = try { LanguageManager.getText("ENGLISH", key) } catch (_: Exception) { null } + val m = Regex("^\\[(.*)]$").matchEntire(t?.trim() ?: "") + val stripped = m?.groupValues?.get(1) ?: t + if (stripped == null || 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") } + } + for (key in candidates) localizeEnglishNoBrackets(key)?.let { return it } + return raw + } +} diff --git a/app/src/main/java/com/dano/test1/HandlerOpeningScreen.kt b/app/src/main/java/com/dano/test1/HandlerOpeningScreen.kt index d428eba..f9c2ef8 100644 --- a/app/src/main/java/com/dano/test1/HandlerOpeningScreen.kt +++ b/app/src/main/java/com/dano/test1/HandlerOpeningScreen.kt @@ -210,6 +210,7 @@ class HandlerOpeningScreen(private val activity: MainActivity) { } private fun updateButtonTexts() { + // --- dynamische Fragebogen-Buttons wie gehabt --- questionnaireFiles.forEach { (button, fileName) -> val entry = questionnaireEntries.firstOrNull { it.file == fileName } val key = fileName.substringAfter("questionnaire_").substringAfter("_").removeSuffix(".json") @@ -220,23 +221,32 @@ class HandlerOpeningScreen(private val activity: MainActivity) { if (entry?.showPoints == true && pointsAvailable != null) { buttonText += " (${points} P)" } - button.text = buttonText if (entry?.showPoints == true && pointsAvailable != null) { when { - points in 0..12 -> button.setBackgroundColor(Color.parseColor("#4CAF50")) + points in 0..12 -> button.setBackgroundColor(Color.parseColor("#4CAF50")) points in 13..36 -> button.setBackgroundColor(Color.parseColor("#FFEB3B")) - points in 37..100 -> button.setBackgroundColor(Color.parseColor("#F44336")) - else -> button.setBackgroundColor(Color.parseColor("#E0E0E0")) + points in 37..100-> button.setBackgroundColor(Color.parseColor("#F44336")) + else -> button.setBackgroundColor(Color.parseColor("#E0E0E0")) } } else { button.setBackgroundColor(Color.parseColor("#E0E0E0")) } } - buttonLoad.text = LanguageManager.getText(languageID, "load") - databaseButton.text = "Datenbank" // fixierter Text gewünscht + // --- HIER: alle Hauptbuttons nach Sprache neu setzen --- + buttonLoad.text = LanguageManager.getText(languageID, "load") + saveButton.text = LanguageManager.getText(languageID, "save") + editButton.text = LanguageManager.getText(languageID, "edit") + uploadButton.text = LanguageManager.getText(languageID, "upload") + downloadButton.text= LanguageManager.getText(languageID, "download") + databaseButton.text= LanguageManager.getText(languageID, "database") + + // optional: Beispieltext/Hints auch aktualisieren + val hintTag = editText.tag as? String ?: "" + editText.hint = LanguageManager.getText(languageID, hintTag) + textView.text = LanguageManager.getText(languageID, "example_text") } private fun setButtonsEnabled(enabledButtons: List