@ -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,16 +230,10 @@ 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 KEINE Hintergrundfarben, nur Textfarbe
sortedQuestionnaires . forEachIndexed { idx , q ->
// 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 ( )
@ -138,53 +243,55 @@ class DatabaseButtonHandler(
table = tableQ ,
cells = listOf ( ( idx + 1 ) . toString ( ) , q . id , statusText ) ,
onClick = { openQuestionnaireDetailScreen ( clientCode , q . id ) } ,
colorOverrides = mapOf ( 2 to statusTextColor ) , // nur Text einfärben
colorOverrides = mapOf ( 2 to statusTextColor ) ,
cellBgOverrides = emptyMap ( )
)
} else {
addDisabledRow (
table = tableQ ,
cells = listOf ( ( idx + 1 ) . toString ( ) , q . id , statusText ) ,
colorOverrides = mapOf ( 2 to statusTextColor ) , // nur Text einfärben
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
// 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 {
" 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
}
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
}
}