Compare commits

...

55 Commits

Author SHA1 Message Date
67bbc3ea06 reverted refactor change to function 2026-03-02 13:25:55 +01:00
b95977e28d created shared functions in ViewUtils.kt 2026-03-02 13:20:51 +01:00
cc89c77186 directory refactor 2026-03-02 13:08:25 +01:00
07b7b3dc1b fixed online connection error and class scale question bug 2026-02-23 18:20:21 +01:00
d30c94beeb new apk 2025-10-16 13:20:50 +02:00
5b1264293c added dummy accounts, change passwort is now a feature, toast when session takes to long, online frontend fix 2025-10-16 13:19:54 +02:00
39a4811fd2 new apk 2025-10-13 20:10:31 +02:00
8b3bb358e8 changed .xml files, now all text visible 2025-10-13 19:33:44 +02:00
5968bf68d1 new apk 2025-10-13 18:30:51 +02:00
ad09bce68c switch from http zu https 2025-10-10 15:33:44 +02:00
4089841336 glass scale centering 2025-10-10 12:35:29 +02:00
5570710da5 client code laod fix 2025-10-10 12:21:59 +02:00
8d54315fe7 new apk and commands added 2025-10-09 16:29:20 +02:00
ac2e0dabd2 changed button visibility 2025-09-30 16:21:20 +02:00
66122dd6c3 languageManager update 2025-09-29 13:58:15 +02:00
dcfa261c1c added offline mode and improved coachcode system 2025-09-29 13:02:00 +02:00
851676f6c3 added fixed coachcode after login. 2025-09-29 11:59:51 +02:00
cfcb689ffc added session time stamp and online/offline state 2025-09-26 12:28:11 +02:00
bf33501b69 implemented donwload upload handling with username 2025-09-23 16:49:59 +02:00
5f5c766133 added username and password for up- and download 2025-09-23 16:10:19 +02:00
91f6f77b73 überarbeiten der datenbank anzeige 2025-09-23 15:17:34 +02:00
8dc9be20a4 new apk 2025-09-21 19:57:26 +02:00
31e2abecf8 finished opening screen 2025-09-21 19:51:23 +02:00
894823f42a finished opening screen 2025-09-21 19:49:49 +02:00
1ffd09049e changed headerCard 2025-09-19 21:18:35 +02:00
0a04568a7c changed qfinish button design 2025-09-19 17:16:53 +02:00
ca8f6ca8e4 added more buttons 2025-09-19 12:22:00 +02:00
e1cf8b4926 changed next and prev button 2025-09-19 12:15:15 +02:00
8ad939db27 added glass graphik 2025-09-19 11:07:31 +02:00
ac1fbb515d languagemanager update 2025-09-18 12:02:47 +02:00
fe2b05c0fd new apk 2025-09-18 09:32:22 +02:00
7014386953 removed error: Toast.makeText(activity, "Fehlende View: $name", Toast.LENGTH_LONG).show() 2025-09-18 08:50:59 +02:00
4cf840b37a added apk 2025-09-17 13:10:31 +02:00
af9c045341 saving header in downloads 2025-09-17 12:46:23 +02:00
77742275e6 finished header and changed colors. 2025-09-16 15:51:11 +02:00
93d2fa4333 full download (excel) 2025-09-12 10:48:55 +02:00
67c11720b9 worked on header, added language manager and output (only english) 2025-09-12 10:29:38 +02:00
304cabc0d7 started adding header 2025-09-10 09:11:09 +02:00
0dfc5df878 added database feature, to check entries 2025-09-08 13:04:37 +02:00
650a3bb050 fixed wrong loading bug, fixed editing bug 2025-09-08 10:27:23 +02:00
45deee664b fixed rotate screen bug 2025-09-06 20:34:01 +02:00
6aebda0009 changed layout for every device 2025-09-06 20:18:26 +02:00
90e662a330 portrait locked 2025-09-05 09:39:09 +02:00
ed4d747798 changed font size 2025-09-05 08:56:56 +02:00
0992304e59 change aes, now more secure, beacuse no hardcode anymore 2025-09-02 13:01:41 +02:00
073f33a9bb final commit on old gitea 2025-08-29 11:51:25 +02:00
5f568f4c0e added edit mode for every important question layout 2025-08-21 11:54:59 +02:00
a803be05d5 added edit mode for string spinner 2025-08-21 11:43:00 +02:00
0a70cb78fd added edit mode for radio questions 2025-08-21 11:08:02 +02:00
ecb1f9b1a2 added consent_not_signed 2025-08-20 12:22:30 +02:00
4e9338631b improved editing offlien and online. 2025-08-20 11:17:43 +02:00
95f290b46a outsourcing functions to new classes 2025-08-19 13:53:16 +02:00
a1736d0241 added passwort for login and download, token-system 2025-08-18 16:27:11 +02:00
148af18496 changed questionnaire_order.json framework. 2025-08-18 13:36:08 +02:00
6e33d61b1e Added encryption and decryption for the case, that database on the server exists. 2025-08-16 23:54:58 +02:00
93 changed files with 6396 additions and 2161 deletions

View File

@ -4,10 +4,10 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-07-28T06:25:21.461295300Z"> <DropdownSelection timestamp="2025-09-29T10:52:30.282144200Z">
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=HA218GZY" /> <DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\danie\.android\avd\Medium_Phone.avd" />
</handle> </handle>
</Target> </Target>
</DropdownSelection> </DropdownSelection>

13
.idea/deviceManager.xml generated Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>

7
.idea/misc.xml generated
View File

@ -6,4 +6,11 @@
<component name="ProjectType"> <component name="ProjectType">
<option name="id" value="Android" /> <option name="id" value="Android" />
</component> </component>
<component name="VisualizationToolProject">
<option name="state">
<ProjectState>
<option name="scale" value="0.1221923828125" />
</ProjectState>
</option>
</component>
</project> </project>

6
.idea/render.experimental.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RenderSettings">
<option name="showDecorations" value="true" />
</component>
</project>

View File

@ -13,7 +13,7 @@ android {
minSdk = 29 minSdk = 29
targetSdk = 35 targetSdk = 35
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "1.3"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@ -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 { compileOptions {
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11
@ -44,22 +62,31 @@ dependencies {
implementation(libs.material) implementation(libs.material)
implementation(libs.androidx.activity) implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout) implementation(libs.androidx.constraintlayout)
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)
implementation("com.google.code.gson:gson:2.10.1") implementation("com.google.code.gson:gson:2.10.1")
implementation("androidx.room:room-runtime:$room_version") implementation("androidx.room:room-runtime:$room_version")
kapt("androidx.room:room-compiler:$room_version") kapt("androidx.room:room-compiler:$room_version")
implementation("androidx.room:room-ktx:$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 // SQLCipher
implementation ("net.zetetic:android-database-sqlcipher:4.5.3@aar") implementation("net.zetetic:android-database-sqlcipher:4.5.3@aar")
implementation ("androidx.sqlite:sqlite:2.1.0") implementation("androidx.sqlite:sqlite:2.1.0")
implementation ("androidx.sqlite:sqlite-framework:2.1.0") implementation("androidx.sqlite:sqlite-framework:2.1.0")
// Server Upload // Server Upload
implementation("com.squareup.okhttp3:okhttp:4.12.0") 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
} }

BIN
app/release/app-release.apk Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,37 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "com.dano.test1",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 1,
"versionName": "1.3",
"outputFile": "app-release.apk"
}
],
"elementType": "File",
"baselineProfiles": [
{
"minApi": 28,
"maxApi": 30,
"baselineProfiles": [
"baselineProfiles/1/app-release.dm"
]
},
{
"minApi": 31,
"maxApi": 2147483647,
"baselineProfiles": [
"baselineProfiles/0/app-release.dm"
]
}
],
"minSdkVersionForDexing": 29
}

View File

@ -1,8 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools"
<uses-permission android:name="android.permission.INTERNET" /> package="com.dano.test1">
<!-- Netzwerkberechtigungen -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application <application
android:name=".MyApp" android:name=".MyApp"
@ -15,6 +18,7 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.Test1" android:theme="@style/Theme.Test1"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="true"
tools:targetApi="31"> tools:targetApi="31">
<provider <provider
@ -29,7 +33,8 @@
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true"> android:exported="true"
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|screenLayout">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />

Binary file not shown.

View File

@ -20,11 +20,16 @@
}, },
{ {
"id": "q2", "id": "q2",
"layout": "client_not_signed", "layout": "client_coach_code_question",
"textKey1": "no_consent_entered", "question": "no_consent_entered",
"textKey2": "no_consent_note", "hint1": "client_code",
"question": "coach_code_request", "hint2": "coach_code"
"hint": "coach_code" },
{
"id": "last_page",
"layout": "last_page",
"textKey": "finish_data_entry",
"question": "data_final_warning"
}, },
{ {
"id": "q28", "id": "q28",

View File

@ -107,7 +107,7 @@
{ {
"id": "q11", "id": "q11",
"layout": "radio_question", "layout": "radio_question",
"question": "times_happend", "question": "times_happend2",
"options": [ "options": [
{ "key": "once" }, { "key": "once" },
{ "key": "multiple_times" } { "key": "multiple_times" }
@ -116,7 +116,7 @@
{ {
"id": "q12", "id": "q12",
"layout": "value_spinner", "layout": "value_spinner",
"question": "age_at_incident", "question": "age_at_incident2",
"range": { "range": {
"min": 1, "min": 1,
"max": 122 "max": 122

View File

@ -1,24 +1,45 @@
[ [
{ {
"file": "questionnaire_1_demographic_information.json", "file": "questionnaire_1_demographic_information.json",
"showPoints": false "showPoints": false,
"condition": {
"alwaysAvailable": true
}
}, },
{ {
"file": "questionnaire_2_rhs.json", "file": "questionnaire_2_rhs.json",
"showPoints": true "showPoints": true,
"condition": {
"alwaysAvailable": true
}
}, },
{ {
"file": "questionnaire_3_integration_index.json", "file": "questionnaire_3_integration_index.json",
"showPoints": true "showPoints": true,
"condition": {
"alwaysAvailable": true
}
}, },
{ {
"file": "questionnaire_4_consultation_results.json", "file": "questionnaire_4_consultation_results.json",
"showPoints": false "showPoints": false,
"condition": {
"requiresCompleted": [
"questionnaire_1_demographic_information",
"questionnaire_2_rhs",
"questionnaire_3_integration_index"
],
"questionnaire": "questionnaire_1_demographic_information",
"questionId": "consent_instruction",
"operator": "==",
"value": "consent_signed"
}
}, },
{ {
"file": "questionnaire_5_final_interview.json", "file": "questionnaire_5_final_interview.json",
"showPoints": false, "showPoints": false,
"condition": { "condition": {
"requiresCompleted": ["questionnaire_4_consultation_results"],
"questionnaire": "questionnaire_4_consultation_results", "questionnaire": "questionnaire_4_consultation_results",
"questionId": "consultation_decision", "questionId": "consultation_decision",
"operator": "==", "operator": "==",
@ -27,6 +48,20 @@
}, },
{ {
"file": "questionnaire_6_follow_up_survey.json", "file": "questionnaire_6_follow_up_survey.json",
"showPoints": false "showPoints": true,
"condition": {
"anyOf": [
{
"requiresCompleted": ["questionnaire_5_final_interview"]
},
{
"requiresCompleted": ["questionnaire_4_consultation_results"],
"questionnaire": "questionnaire_4_consultation_results",
"questionId": "consultation_decision",
"operator": "!=",
"value": "yellow"
}
]
}
} }
] ]

View File

@ -1,56 +0,0 @@
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import javax.crypto.Cipher
import javax.crypto.CipherInputStream
import javax.crypto.CipherOutputStream
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import kotlin.random.Random
object AES256Helper {
private const val TRANSFORMATION = "AES/CBC/PKCS5Padding"
private const val ALGORITHM = "AES"
private const val IV_SIZE = 16
// Beispiel-Key: 32 Bytes = 256 bit. Ersetze das durch deinen eigenen sicheren Schlüssel!
private val keyBytes = "12345678901234567890123456789012".toByteArray(Charsets.UTF_8)
private val secretKey = SecretKeySpec(keyBytes, ALGORITHM)
// Verschlüsseln: InputFile -> OutputFile (mit zufälligem IV vorne in der Datei)
fun encryptFile(inputFile: File, outputFile: File) {
val iv = ByteArray(IV_SIZE)
Random.nextBytes(iv)
val ivSpec = IvParameterSpec(iv)
val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec)
FileOutputStream(outputFile).use { fileOut ->
// IV vorne reinschreiben
fileOut.write(iv)
CipherOutputStream(fileOut, cipher).use { cipherOut ->
FileInputStream(inputFile).use { fileIn ->
fileIn.copyTo(cipherOut)
}
}
}
}
// Entschlüsseln: InputFile (IV+Ciphertext) -> OutputFile (Klartext)
fun decryptFile(inputFile: File, outputFile: File) {
FileInputStream(inputFile).use { fileIn ->
val iv = ByteArray(IV_SIZE)
if (fileIn.read(iv) != IV_SIZE) throw IllegalArgumentException("Ungültige Datei oder IV fehlt")
val ivSpec = IvParameterSpec(iv)
val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec)
CipherInputStream(fileIn, cipher).use { cipherIn ->
FileOutputStream(outputFile).use { fileOut ->
cipherIn.copyTo(fileOut)
}
}
}
}
}

View File

@ -1,81 +0,0 @@
package com.dano.test1
import android.content.Context
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.File
import java.io.FileOutputStream
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
object DatabaseDownloader {
private const val DB_NAME = "questionnaire_database"
private const val API_TOKEN = "MEIN_SUPER_GEHEIMES_TOKEN_12345"
private const val SERVER_DOWNLOAD_URL = "http://49.13.157.44/downloadFull.php?token=$API_TOKEN"
// AES-256 Key (muss exakt 32 Bytes lang sein)
private const val AES_KEY = "12345678901234567890123456789012"
private val client = OkHttpClient()
fun downloadAndReplaceDatabase(context: Context) {
CoroutineScope(Dispatchers.IO).launch {
try {
Log.d("DOWNLOAD", "Download gestartet: $SERVER_DOWNLOAD_URL")
val request = Request.Builder()
.url(SERVER_DOWNLOAD_URL)
.build()
val response = client.newCall(request).execute()
if (!response.isSuccessful) {
Log.e("DOWNLOAD", "Fehler beim Download: ${response.code}")
return@launch
}
// Zwischenspeichern der verschlüsselten Datei
val downloadedFile = File(context.cacheDir, "downloaded_database.enc")
response.body?.byteStream()?.use { input ->
FileOutputStream(downloadedFile).use { output ->
input.copyTo(output)
}
}
Log.d("DOWNLOAD", "Datei gespeichert: ${downloadedFile.absolutePath}")
// Entschlüsselung
val decryptedBytes = decryptFile(downloadedFile)
val dbFile = context.getDatabasePath(DB_NAME)
if (dbFile.exists()) dbFile.delete()
FileOutputStream(dbFile).use { fos ->
fos.write(decryptedBytes)
}
Log.d("DOWNLOAD", "Neue DB erfolgreich entschlüsselt und eingesetzt")
} catch (e: Exception) {
Log.e("DOWNLOAD", "Fehler beim Download oder Ersetzen der DB", e)
}
}
}
private fun decryptFile(file: File): ByteArray {
val fileBytes = file.readBytes()
if (fileBytes.size < 16) throw IllegalArgumentException("Datei zu kurz, kein IV vorhanden")
val iv = fileBytes.copyOfRange(0, 16)
val cipherBytes = fileBytes.copyOfRange(16, fileBytes.size)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
val keySpec = SecretKeySpec(AES_KEY.toByteArray(Charsets.UTF_8), "AES")
val ivSpec = IvParameterSpec(iv)
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec)
return cipher.doFinal(cipherBytes)
}
}

View File

@ -1,232 +0,0 @@
package com.dano.test1
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.util.Base64
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import android.database.Cursor
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.asRequestBody
import org.json.JSONArray
import org.json.JSONObject
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import kotlin.system.exitProcess
object DatabaseUploader {
private const val DB_NAME = "questionnaire_database"
// TODO entferne uploadDeltaTest2.php
private const val SERVER_DELTA_URL = "http://49.13.157.44/uploadDeltaTest2.php"
private const val SERVER_CHECK_URL = "http://49.13.157.44/checkDatabaseExists.php"
private const val API_TOKEN = "MEIN_SUPER_GEHEIMES_TOKEN_12345"
private val client = OkHttpClient()
fun uploadDatabase(context: Context) {
CoroutineScope(Dispatchers.IO).launch {
try {
val dbFile = context.getDatabasePath(DB_NAME)
if (!dbFile.exists()) {
Log.e("UPLOAD", "Datenbankdatei existiert nicht: ${dbFile.absolutePath}")
return@launch
}
// WAL-Checkpoint
try {
val db = SQLiteDatabase.openDatabase(
dbFile.absolutePath,
null,
SQLiteDatabase.OPEN_READWRITE
)
db.rawQuery("PRAGMA wal_checkpoint(FULL);", null).use { cursor ->
if (cursor.moveToFirst()) {
try {
Log.d("UPLOAD", "WAL-Checkpoint result: ${cursor.getInt(0)}")
} catch (_: Exception) {}
}
}
db.close()
Log.d("UPLOAD", "WAL-Checkpoint erfolgreich.")
} catch (e: Exception) {
Log.e("UPLOAD", "Fehler beim WAL-Checkpoint", e)
}
val exists = checkDatabaseExists()
if (exists) {
Log.d("UPLOAD", "Server-Datenbank vorhanden → Delta-Upload")
uploadPseudoDelta(context, dbFile)
} else {
Log.d("UPLOAD", "Keine Server-Datenbank → Delta-Upload")
uploadPseudoDelta(context, dbFile)
}
} catch (e: Exception) {
Log.e("UPLOAD", "Fehler beim Hochladen der DB", e)
}
}
}
private fun checkDatabaseExists(): Boolean {
return try {
val request = Request.Builder()
.url(SERVER_CHECK_URL)
.get()
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
Log.e("UPLOAD", "checkDatabaseExists HTTP error: ${response.code}")
return false
}
val body = response.body?.string() ?: return false
try {
val j = JSONObject(body)
j.optBoolean("exists", false)
} catch (e: Exception) {
body.contains("exists", ignoreCase = true)
}
}
} catch (e: Exception) {
Log.e("UPLOAD", "Fehler bei Server-Prüfung", e)
false
}
}
/**
* Wichtig: Diese Funktion wurde erweitert, sodass:
* - die DB als JSON in eine temporäre Datei geschrieben wird,
* - diese JSON-Datei AES-verschlüsselt wird (mit AES256Helper.encryptFile),
* - die verschlüsselte Datei als Multipart 'file' an den Server gesendet wird.
*
* (Funktionalität: gleiche Signatur wie vorher behalten)
*/
private fun uploadPseudoDelta(context: Context, file: File) {
try {
val db = SQLiteDatabase.openDatabase(file.absolutePath, null, SQLiteDatabase.OPEN_READONLY)
val data = JSONObject().apply {
put("clients", queryToJsonArray(db, "SELECT clientCode FROM clients"))
put("questionnaires", queryToJsonArray(db, "SELECT id FROM questionnaires"))
put("questions", queryToJsonArray(db, "SELECT questionId, questionnaireId, question FROM questions"))
put("answers", queryToJsonArray(db, "SELECT clientCode, questionId, answerValue FROM answers"))
put(
"completed_questionnaires",
queryToJsonArray(
db,
"SELECT clientCode, questionnaireId, timestamp, isDone, sumPoints FROM completed_questionnaires"
)
)
}
db.close()
// Schreibe JSON in temporäre Datei
val tmpJson = File(context.cacheDir, "payload.json")
tmpJson.writeText(data.toString())
// Verschlüssele JSON -> tmpEnc
val tmpEnc = File(context.cacheDir, "payload.enc")
try {
AES256Helper.encryptFile(tmpJson, tmpEnc)
} catch (e: Exception) {
Log.e("UPLOAD", "Fehler bei der Verschlüsselung der JSON-Datei", e)
// cleanup
tmpJson.delete()
return
}
val requestBody = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("token", API_TOKEN)
// Datei-Feld "file" mit verschlüsselter Payload
.addFormDataPart(
"file",
"payload.enc",
tmpEnc.asRequestBody("application/octet-stream".toMediaType())
)
.build()
val request = Request.Builder()
.url(SERVER_DELTA_URL)
.post(requestBody)
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.e("UPLOAD", "Delta-Upload fehlgeschlagen: ${e.message}")
// cleanup
tmpJson.delete()
tmpEnc.delete()
}
override fun onResponse(call: Call, response: Response) {
val body = try {
response.body?.string() ?: "Keine Response"
} catch (e: Exception) {
"Fehler beim Lesen der Response: ${e.message}"
}
if (response.isSuccessful) {
Log.d("UPLOAD", "Delta-Upload erfolgreich: $body")
// Lösche Hauptdatenbank
if (file.delete()) {
Log.d("UPLOAD", "Lokale DB gelöscht.")
} else {
Log.e("UPLOAD", "Löschen der lokalen DB fehlgeschlagen.")
}
// Lösche Journal-Datei
val journalFile = File(file.parent, file.name + "-journal")
if (journalFile.exists() && journalFile.delete()) {
Log.d("UPLOAD", "Journal-Datei gelöscht.")
}
// cleanup temp files
tmpJson.delete()
tmpEnc.delete()
exitProcess(0)
} else {
Log.e("UPLOAD", "Delta-Upload fehlgeschlagen: ${response.code} $body")
tmpJson.delete()
tmpEnc.delete()
}
}
})
} catch (e: Exception) {
Log.e("UPLOAD", "Fehler beim Delta-Upload", e)
}
}
private fun queryToJsonArray(db: SQLiteDatabase, query: String): JSONArray {
val cursor = db.rawQuery(query, null)
val jsonArray = JSONArray()
cursor.use {
val columnNames = it.columnNames
while (it.moveToNext()) {
val obj = JSONObject()
for (col in columnNames) {
val idx = it.getColumnIndex(col)
if (idx >= 0) {
when (it.getType(idx)) {
Cursor.FIELD_TYPE_INTEGER -> obj.put(col, it.getLong(idx))
Cursor.FIELD_TYPE_FLOAT -> obj.put(col, it.getDouble(idx))
Cursor.FIELD_TYPE_STRING -> obj.put(col, it.getString(idx))
Cursor.FIELD_TYPE_NULL -> obj.put(col, JSONObject.NULL)
Cursor.FIELD_TYPE_BLOB -> {
val blob = it.getBlob(idx)
obj.put(col, Base64.encodeToString(blob, Base64.NO_WRAP))
}
else -> obj.put(col, it.getString(idx))
}
}
}
jsonArray.put(obj)
}
}
return jsonArray
}
}

View File

@ -1,116 +0,0 @@
package com.dano.test1
import android.view.View
import android.widget.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class HandlerClientCoachCode(
private val answers: MutableMap<String, Any>,
private val languageID: String,
private val goToNextQuestion: () -> Unit,
private val goToPreviousQuestion: () -> Unit,
private val showToast: (String) -> Unit,
) : QuestionHandler {
private lateinit var question: QuestionItem.ClientCoachCodeQuestion
private lateinit var layout: View
override fun bind(layout: View, question: QuestionItem) {
if (question !is QuestionItem.ClientCoachCodeQuestion) return
this.layout = layout
this.question = question
// Bind UI components
val clientCodeField = layout.findViewById<EditText>(R.id.client_code)
val coachCodeField = layout.findViewById<EditText>(R.id.coach_code)
val questionTextView = layout.findViewById<TextView>(R.id.question)
// Fill question text using language manager
questionTextView.text = question.question?.let {
LanguageManager.getText(languageID, it)
} ?: ""
// Load last used client code if available
val lastClientCode = GlobalValues.LAST_CLIENT_CODE
if (!lastClientCode.isNullOrBlank()) {
clientCodeField.setText(lastClientCode)
clientCodeField.isEnabled = false
} else {
clientCodeField.setText(answers["client_code"] as? String ?: "")
clientCodeField.isEnabled = true
}
// Load saved coach code
coachCodeField.setText(answers["coach_code"] as? String ?: "")
// Set click listener for Next button
layout.findViewById<Button>(R.id.Qnext).setOnClickListener {
onNextClicked(clientCodeField, coachCodeField)
}
// Set click listener for Previous button
layout.findViewById<Button>(R.id.Qprev).setOnClickListener {
onPreviousClicked(clientCodeField, coachCodeField)
}
}
// Handle Next button click
private fun onNextClicked(clientCodeField: EditText, coachCodeField: EditText) {
if (!validate()) {
val message = LanguageManager.getText(languageID, "fill_both_fields")
showToast(message)
return
}
val clientCode = clientCodeField.text.toString()
val coachCode = coachCodeField.text.toString()
// Check if client code already exists asynchronously
CoroutineScope(Dispatchers.IO).launch {
val existingClient = MyApp.database.clientDao().getClientByCode(clientCode)
withContext(Dispatchers.Main) {
if (existingClient != null && clientCodeField.isEnabled) {
// Client code already exists and field was editable
val message = LanguageManager.getText(languageID, "client_code_exists")
showToast(message)
} else {
// Either no existing client or re-using previous code
saveAnswers(clientCode, coachCode)
goToNextQuestion()
}
}
}
}
// Handle Previous button click
private fun onPreviousClicked(clientCodeField: EditText, coachCodeField: EditText) {
val clientCode = clientCodeField.text.toString()
val coachCode = coachCodeField.text.toString()
saveAnswers(clientCode, coachCode)
goToPreviousQuestion()
}
// Validate that both fields are filled
override fun validate(): Boolean {
val clientCode = layout.findViewById<EditText>(R.id.client_code).text
val coachCode = layout.findViewById<EditText>(R.id.coach_code).text
return clientCode.isNotBlank() && coachCode.isNotBlank()
}
// Save answers to shared state and global value
private fun saveAnswers(clientCode: String, coachCode: String) {
GlobalValues.LAST_CLIENT_CODE = clientCode
answers["client_code"] = clientCode
answers["coach_code"] = coachCode
}
// Required override but not used here
override fun saveAnswer() {
// Not used
}
}

View File

@ -1,167 +0,0 @@
package com.dano.test1
import android.content.Context
import android.view.Gravity
import android.view.View
import android.widget.*
class HandlerGlassScaleQuestion(
private val context: Context,
private val answers: MutableMap<String, Any>,
private val points: MutableList<Int>,
private val languageID: String,
private val goToNextQuestion: () -> Unit,
private val goToPreviousQuestion: () -> Unit,
private val showToast: (String) -> Unit
) : QuestionHandler {
private lateinit var layout: View
private lateinit var question: QuestionItem.GlassScaleQuestion
private val scaleLabels = listOf(
"never_glass", "little_glass", "moderate_glass", "much_glass", "extreme_glass"
)
private val pointsMap = mapOf(
"never_glass" to 0,
"little_glass" to 1,
"moderate_glass" to 2,
"much_glass" to 3,
"extreme_glass" to 4
)
override fun bind(layout: View, question: QuestionItem) {
if (question !is QuestionItem.GlassScaleQuestion) return
this.layout = layout
this.question = question
layout.findViewById<TextView>(R.id.textView).text =
question.textKey?.let { LanguageManager.getText(languageID, it) } ?: ""
layout.findViewById<TextView>(R.id.question).text =
question.question?.let { LanguageManager.getText(languageID, it) } ?: ""
val tableLayout = layout.findViewById<TableLayout>(R.id.glass_table)
tableLayout.removeAllViews()
val headerRow = TableRow(context).apply {
layoutParams = TableLayout.LayoutParams(
TableLayout.LayoutParams.MATCH_PARENT,
TableLayout.LayoutParams.WRAP_CONTENT
)
gravity = Gravity.CENTER
}
val emptyCell = TextView(context).apply {
layoutParams = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT, 4f)
}
headerRow.addView(emptyCell)
scaleLabels.forEach { labelKey ->
val labelText = LanguageManager.getText(languageID, labelKey)
val labelView = TextView(context).apply {
text = labelText
gravity = Gravity.START
layoutParams = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT, 1f)
}
headerRow.addView(labelView)
}
tableLayout.addView(headerRow)
addSymptomRows(tableLayout)
layout.findViewById<Button>(R.id.Qnext).setOnClickListener {
if (validate()) {
saveAnswer()
goToNextQuestion()
} else {
val message = LanguageManager.getText(languageID, "select_one_answer_per_row")
showToast(message)
}
}
layout.findViewById<Button>(R.id.Qprev).setOnClickListener {
goToPreviousQuestion()
}
}
private fun addSymptomRows(table: TableLayout) {
question.symptoms.forEach { symptomKey ->
val savedLabel = answers[symptomKey] as? String
val row = TableRow(context).apply {
layoutParams = TableRow.LayoutParams(
TableRow.LayoutParams.MATCH_PARENT,
TableRow.LayoutParams.WRAP_CONTENT
)
}
val symptomText = TextView(context).apply {
text = LanguageManager.getText(languageID, symptomKey)
layoutParams = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT, 4f)
setPadding(4, 16, 4, 16)
}
row.addView(symptomText)
val radioGroup = RadioGroup(context).apply {
orientation = RadioGroup.HORIZONTAL
layoutParams = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT, 5f)
}
scaleLabels.forEach { labelKey ->
val radioButton = RadioButton(context).apply {
tag = labelKey
id = View.generateViewId()
isChecked = savedLabel == labelKey
layoutParams =
RadioGroup.LayoutParams(0, RadioGroup.LayoutParams.WRAP_CONTENT, 1f)
gravity = Gravity.CENTER
}
radioGroup.addView(radioButton)
}
row.addView(radioGroup)
table.addView(row)
}
}
override fun validate(): Boolean {
val table = layout.findViewById<TableLayout>(R.id.glass_table)
for (i in 1 until table.childCount) {
val row = table.getChildAt(i) as TableRow
val radioGroup = row.getChildAt(1) as RadioGroup
if (radioGroup.checkedRadioButtonId == -1) {
return false
}
}
return true
}
override fun saveAnswer() {
// Vorherige Punkte dieser Frage entfernen
question.symptoms.forEach {
val previousLabel = answers[it] as? String
val previousPoint = pointsMap[previousLabel]
if (previousPoint != null) {
points.remove(previousPoint)
}
}
val table = layout.findViewById<TableLayout>(R.id.glass_table)
for (i in 1 until table.childCount) {
val row = table.getChildAt(i) as TableRow
val symptomKey = question.symptoms[i - 1]
val radioGroup = row.getChildAt(1) as RadioGroup
val checkedId = radioGroup.checkedRadioButtonId
if (checkedId != -1) {
val radioButton = radioGroup.findViewById<RadioButton>(checkedId)
val selectedLabel = radioButton.tag as String
answers[symptomKey] = selectedLabel
val point = pointsMap[selectedLabel] ?: 0
points.add(point)
}
}
}
}

View File

@ -1,93 +0,0 @@
package com.dano.test1
import android.view.View
import android.widget.*
import android.text.Html
import kotlinx.coroutines.*
class HandlerLastPage(
private val answers: Map<String, Any>,
private val languageID: String,
private val goToNextQuestion: () -> Unit,
private val goToPreviousQuestion: () -> Unit,
private val saveAnswersToDatabase: suspend (Map<String, Any>) -> Unit
) : QuestionHandler {
private lateinit var currentQuestion: QuestionItem.LastPage
private lateinit var layout: View
private val minLoadingTimeMs = 2000L // Minimum loading time in milliseconds (2 seconds)
override fun bind(layout: View, question: QuestionItem) {
this.layout = layout
currentQuestion = question as QuestionItem.LastPage
// Set localized text for the last page
layout.findViewById<TextView>(R.id.textView).text =
LanguageManager.getText(languageID, currentQuestion.textKey)
// Set question text with HTML formatting
layout.findViewById<TextView>(R.id.question).text =
Html.fromHtml(
LanguageManager.getText(languageID, currentQuestion.question),
Html.FROM_HTML_MODE_LEGACY
)
// Setup previous button
layout.findViewById<Button>(R.id.Qprev).setOnClickListener {
goToPreviousQuestion()
}
// Setup finish button
layout.findViewById<Button>(R.id.Qfinish).setOnClickListener {
showLoading(true) // Show loading indicator
// Save answers on a background thread
CoroutineScope(Dispatchers.IO).launch {
val startTime = System.currentTimeMillis()
// Save answers to database (suspend function)
saveAnswersToDatabase(answers)
// Calculate total points and update global value
GlobalValues.INTEGRATION_INDEX = sumPoints()
// Save last client code globally if available
val clientCode = answers["client_code"] as? String
if (clientCode != null) GlobalValues.LAST_CLIENT_CODE = clientCode
// Ensure loading animation runs at least 2 seconds
val elapsedTime = System.currentTimeMillis() - startTime
if (elapsedTime < minLoadingTimeMs) {
delay(minLoadingTimeMs - elapsedTime)
}
// Switch back to main thread to update UI
withContext(Dispatchers.Main) {
showLoading(false) // Hide loading indicator
val activity = layout.context as? MainActivity
activity?.finishQuestionnaire() ?: goToNextQuestion()
}
}
}
}
override fun validate(): Boolean = true // No validation needed on last page
override fun saveAnswer() {} // No answers to save here
// Calculate the sum of all keys ending with "_points"
private fun sumPoints(): Int =
answers.filterKeys { it.endsWith("_points") }
.values.mapNotNull { it as? Int }
.sum()
// Show or hide a ProgressBar (loading spinner)
private fun showLoading(show: Boolean) {
val progressBar = layout.findViewById<ProgressBar>(R.id.progressBar)
val finishButton = layout.findViewById<Button>(R.id.Qfinish)
val prevButton = layout.findViewById<Button>(R.id.Qprev)
progressBar?.visibility = if (show) View.VISIBLE else View.GONE
finishButton?.isEnabled = !show
prevButton?.isEnabled = !show
}
}

View File

@ -1,104 +0,0 @@
package com.dano.test1
import android.content.Context
import android.view.View
import android.widget.*
class HandlerMultiCheckboxQuestion(
private val context: Context,
private val answers: MutableMap<String, Any>,
private val points: MutableList<Int>,
private val languageID: String,
private val goToNextQuestion: () -> Unit,
private val goToPreviousQuestion: () -> Unit,
private val showToast: (String) -> Unit
) : QuestionHandler {
private lateinit var layout: View
private lateinit var question: QuestionItem.MultiCheckboxQuestion
override fun bind(layout: View, question: QuestionItem) {
this.layout = layout
this.question = question as QuestionItem.MultiCheckboxQuestion
val container = layout.findViewById<LinearLayout>(R.id.CheckboxContainer)
val questionTitle = layout.findViewById<TextView>(R.id.question)
val questionTextView = layout.findViewById<TextView>(R.id.textView)
// Hier jetzt identisch zur RadioQuestion:
questionTextView.text = this.question.textKey?.let { LanguageManager.getText(languageID, it) } ?: ""
questionTitle.text = this.question.question?.let { LanguageManager.getText(languageID, it) } ?: ""
container.removeAllViews()
val selectedKeys = this.question.question?.let {
(answers[it] as? List<*>)?.map { it.toString() }?.toSet()
} ?: emptySet()
this.question.options.forEach { option ->
val checkBox = CheckBox(context).apply {
text = LanguageManager.getText(languageID, option.key)
tag = option.key
isChecked = selectedKeys.contains(option.key)
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
val scale = context.resources.displayMetrics.density
val marginBottom = (16 * scale + 0.5f).toInt()
setMargins(0, 0, 0, marginBottom)
}
}
container.addView(checkBox)
}
layout.findViewById<Button>(R.id.Qnext).setOnClickListener {
if (validate()) {
saveAnswer()
goToNextQuestion()
} else {
val msgKey = if (question.minSelection == 1) {
"select_at_least_one_answer"
} else {
"select_at_least_minimum"
}
val errorMessage = LanguageManager.getTextFormatted(languageID, msgKey, "choose_more_elements")
showToast(errorMessage)
}
}
layout.findViewById<Button>(R.id.Qprev).setOnClickListener {
goToPreviousQuestion()
}
}
override fun validate(): Boolean {
val container = layout.findViewById<LinearLayout>(R.id.CheckboxContainer)
var selectedCount = 0
for (i in 0 until container.childCount) {
val checkBox = container.getChildAt(i) as? CheckBox ?: continue
if (checkBox.isChecked) selectedCount++
}
return selectedCount >= question.minSelection
}
override fun saveAnswer() {
val container = layout.findViewById<LinearLayout>(R.id.CheckboxContainer)
val selectedKeys = mutableListOf<String>()
var totalPoints = 0
for (i in 0 until container.childCount) {
val checkBox = container.getChildAt(i) as? CheckBox ?: continue
if (checkBox.isChecked) {
val key = checkBox.tag.toString()
selectedKeys.add(key)
totalPoints += question.pointsMap?.get(key) ?: 0
}
}
question.question?.let { questionKey ->
answers[questionKey] = selectedKeys
points.add(totalPoints)
}
}
}

View File

@ -1,610 +0,0 @@
package com.dano.test1
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Color
import android.graphics.pdf.PdfDocument
import android.view.View
import android.widget.*
import kotlinx.coroutines.*
import org.json.JSONArray
import android.util.Log
import java.io.File
var INTEGRATION_INDEX_POINTS: Int? = null
class HandlerOpeningScreen(private val activity: MainActivity) {
private var languageID: String = "GERMAN"
private lateinit var editText: EditText
private lateinit var spinner: Spinner
private lateinit var textView: TextView
private lateinit var buttonContainer: LinearLayout
private lateinit var buttonLoad: Button
private lateinit var saveButton: Button
private lateinit var editButton: Button
private lateinit var uploadButton: Button
private lateinit var downloadButton: Button
private val dynamicButtons = mutableListOf<Button>()
private val questionnaireFiles = mutableMapOf<Button, String>()
private val buttonPoints: MutableMap<String, Int> = mutableMapOf()
private var questionnaireEntries: List<QuestionItem.QuestionnaireEntry> = emptyList()
fun init() {
activity.setContentView(R.layout.opening_screen)
bindViews()
loadQuestionnaireOrder()
createQuestionnaireButtons()
restorePreviousClientCode()
setupLanguageSpinner()
setupLoadButton()
setupSaveButton()
setupEditButton()
setupUploadButton()
setupDownloadButton()
val dbPath = "/data/data/com.dano.test1/databases/questionnaire_database"
val pathExists = File(dbPath).exists()
if (pathExists) {
updateMainButtonsState(true)
}
else{
updateMainButtonsState(false)
}
if (!editText.text.isNullOrBlank()) {
buttonLoad.performClick()
}
}
private fun bindViews() {
editText = activity.findViewById(R.id.editText)
spinner = activity.findViewById(R.id.string_spinner1)
textView = activity.findViewById(R.id.textView)
buttonContainer = activity.findViewById(R.id.buttonContainer)
buttonLoad = activity.findViewById(R.id.loadButton)
saveButton = activity.findViewById(R.id.saveButton)
editButton = activity.findViewById(R.id.editButton)
uploadButton = activity.findViewById(R.id.uploadButton)
downloadButton = activity.findViewById(R.id.downloadButton)
val tag = editText.tag as? String ?: ""
editText.hint = LanguageManager.getText(languageID, tag)
textView.text = LanguageManager.getText(languageID, "example_text")
}
private fun loadQuestionnaireOrder() {
try {
val inputStream = activity.assets.open("questionnaire_order.json")
val json = inputStream.bufferedReader().use { it.readText() }
val jsonArray = JSONArray(json)
questionnaireEntries = (0 until jsonArray.length()).map { i ->
val obj = jsonArray.getJSONObject(i)
val file = obj.getString("file")
val conditionObj = obj.optJSONObject("condition")
val condition = if (conditionObj != null) {
QuestionItem.Condition(
questionnaire = conditionObj.getString("questionnaire"),
questionId = conditionObj.getString("questionId"),
operator = conditionObj.getString("operator"),
value = conditionObj.getString("value")
)
} else null
val showPoints = obj.optBoolean("showPoints", false)
QuestionItem.QuestionnaireEntry(file, condition, showPoints)
}
} catch (e: Exception) {
e.printStackTrace()
questionnaireEntries = emptyList()
}
}
private fun createQuestionnaireButtons() {
buttonContainer.removeAllViews()
dynamicButtons.clear()
questionnaireFiles.clear()
for ((index, entry) in questionnaireEntries.withIndex()) {
val button = Button(activity).apply {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply { setMargins(0, 8, 0, 8) }
text = "Questionnaire ${index + 1}"
id = View.generateViewId()
}
buttonContainer.addView(button)
dynamicButtons.add(button)
questionnaireFiles[button] = entry.file
}
updateButtonTexts()
setButtonsEnabled(listOf(dynamicButtons.firstOrNull()).filterNotNull())
dynamicButtons.forEach { button ->
button.setOnClickListener {
startQuestionnaireForButton(button)
setButtonsEnabled(dynamicButtons.filter { it != button })
}
}
}
private fun restorePreviousClientCode() {
GlobalValues.LAST_CLIENT_CODE?.let { editText.setText(it) }
}
private fun setupLanguageSpinner() {
val languages = listOf("GERMAN", "ENGLISH", "FRENCH", "ROMANIAN", "ARABIC", "POLISH", "TURKISH", "UKRAINIAN", "RUSSIAN", "SPANISH")
val adapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, languages).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
}
spinner.adapter = adapter
spinner.setSelection(languages.indexOf(languageID))
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
languageID = languages[position]
updateButtonTexts()
val hintTag = editText.tag as? String ?: ""
editText.hint = LanguageManager.getText(languageID, hintTag)
}
override fun onNothingSelected(parent: AdapterView<*>) {}
}
}
private fun setupLoadButton() {
buttonLoad.text = LanguageManager.getText(languageID, "load")
buttonLoad.setOnClickListener { handleLoadButton() }
}
private fun handleLoadButton() {
buttonPoints.clear()
updateButtonTexts()
setButtonsEnabled(emptyList())
val inputText = editText.text.toString().trim()
if (inputText.isBlank()) {
val message = LanguageManager.getText(languageID, "please_client_code")
Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
return
}
val clientCode = inputText
GlobalValues.LAST_CLIENT_CODE = clientCode
CoroutineScope(Dispatchers.IO).launch {
val client = MyApp.database.clientDao().getClientByCode(clientCode)
if (client == null) {
withContext(Dispatchers.Main) {
val message = LanguageManager.getText(languageID, "no_profile")
Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
setButtonsEnabled(listOf(dynamicButtons.firstOrNull()).filterNotNull())
}
return@launch
}
withContext(Dispatchers.Main) {
updateMainButtonsState(true) // Datenbank vorhanden -> Buttons aktivieren
handleNormalLoad(clientCode)
}
}
}
private suspend fun handleNormalLoad(clientCode: String) {
val completedIds = withContext(Dispatchers.IO) {
MyApp.database.completedQuestionnaireDao().getCompletedQuestionnairesForClient(clientCode)
}
if (completedIds.isEmpty()) {
setButtonsEnabled(listOf(dynamicButtons.firstOrNull()).filterNotNull())
val message = LanguageManager.getText(languageID, "no_profile")
Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
return
}
val completedIndexes = completedIds.mapNotNull { id ->
questionnaireEntries.indexOfFirst { it.file.contains(id, ignoreCase = true) }.takeIf { it >= 0 }
}.sorted()
val completedEntries = withContext(Dispatchers.IO) {
MyApp.database.completedQuestionnaireDao().getAllForClient(clientCode)
}
buttonPoints.clear()
for (entry in completedEntries) {
if (entry.isDone) {
buttonPoints[entry.questionnaireId] = entry.sumPoints ?: 0
if (entry.questionnaireId.contains("questionnaire_3_integration_index", ignoreCase = true)) {
INTEGRATION_INDEX_POINTS = entry.sumPoints
}
}
}
updateButtonTexts()
var nextIndex = (completedIndexes.lastOrNull() ?: -1) + 1
while (nextIndex < questionnaireEntries.size) {
val entry = questionnaireEntries[nextIndex]
val condition = entry.condition
if (condition != null) {
val answers = MyApp.database.answerDao().getAnswersForClientAndQuestionnaire(clientCode, condition.questionnaire)
val relevantAnswer = answers.find {
it.questionId.endsWith(condition.questionId)
}
val answerValue = relevantAnswer?.answerValue ?: ""
val conditionMet = when (condition.operator) {
"!=" -> answerValue != condition.value
"==" -> answerValue == condition.value
else -> true
}
if (conditionMet) break
else nextIndex++
} else {
break
}
}
if (nextIndex >= questionnaireEntries.size) {
setButtonsEnabled(emptyList())
val message = LanguageManager.getText(languageID, "questionnaires_finished")
Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
} else {
val nextFileName = questionnaireEntries[nextIndex].file
val nextButton = questionnaireFiles.entries.firstOrNull { it.value == nextFileName }?.key
setButtonsEnabled(listOfNotNull(nextButton))
}
}
private fun updateButtonTexts() {
questionnaireFiles.forEach { (button, fileName) ->
val entry = questionnaireEntries.firstOrNull { it.file == fileName }
val key = fileName.substringAfter("questionnaire_").substringAfter("_").removeSuffix(".json")
var buttonText = LanguageManager.getText(languageID, key)
val pointsAvailable = buttonPoints.entries.firstOrNull { fileName.contains(it.key, ignoreCase = true) }
val points = pointsAvailable?.value ?: 0
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")) // Grün
points in 13..36 -> button.setBackgroundColor(Color.parseColor("#FFEB3B")) // Gelb
points in 37..100 -> button.setBackgroundColor(Color.parseColor("#F44336")) // Rot
else -> button.setBackgroundColor(Color.parseColor("#E0E0E0")) // Grau bei 0 Punkten
}
} else {
button.setBackgroundColor(Color.parseColor("#E0E0E0"))
}
}
buttonLoad.text = LanguageManager.getText(languageID, "load")
}
private fun setButtonsEnabled(enabledButtons: List<Button>) {
questionnaireFiles.keys.forEach { button ->
button.isEnabled = enabledButtons.contains(button)
button.alpha = if (button.isEnabled) 1.0f else 0.5f
}
}
private fun startQuestionnaireForButton(button: Button) {
val fileName = questionnaireFiles[button] ?: return
val questionnaire = QuestionnaireGeneric(fileName)
startQuestionnaire(questionnaire)
}
private fun startQuestionnaire(questionnaire: QuestionnaireBase<*>) {
activity.startQuestionnaire(questionnaire, languageID)
}
fun onBackPressed(): Boolean = false
private fun showCompletedQuestionnaires(clientCode: String) {
CoroutineScope(Dispatchers.IO).launch {
val actualClientCode = clientCode.removeSuffix("_database")
val completedEntries = MyApp.database.completedQuestionnaireDao().getAllForClient(actualClientCode)
Log.d("PDF_DEBUG", "Completed entries for client $actualClientCode:")
for (entry in completedEntries) {
Log.d("PDF_DEBUG", "Questionnaire ID: ${entry.questionnaireId}, Done: ${entry.isDone}, Points: ${entry.sumPoints}")
}
val pdfDocument = PdfDocument()
val pageWidth = 595
val pageHeight = 842
val paint = Paint().apply { textSize = 12f }
val csvBuilder = StringBuilder()
csvBuilder.appendLine("ClientCode,QuestionnaireID,IsDone,Points,Question,Answer")
for ((index, entry) in completedEntries.withIndex()) {
val pageInfo = PdfDocument.PageInfo.Builder(pageWidth, pageHeight, index + 1).create()
var page = pdfDocument.startPage(pageInfo)
var canvas = page.canvas
var yPosition = 40f
canvas.drawText("Client Code: $actualClientCode", 20f, yPosition, paint)
yPosition += 20f
canvas.drawText("Questionnaire: ${entry.questionnaireId}", 20f, yPosition, paint)
yPosition += 20f
canvas.drawText("Status: ${entry.isDone}", 20f, yPosition, paint)
yPosition += 20f
canvas.drawText("Points: ${entry.sumPoints ?: "N/A"}", 20f, yPosition, paint)
yPosition += 30f
val answers = MyApp.database.answerDao().getAnswersForClientAndQuestionnaire(actualClientCode, entry.questionnaireId)
for (answer in answers) {
val questionKey = answer.questionId.substringAfter("-")
val questionText = LanguageManager.getText("ENGLISH", questionKey)
val rawAnswerText = LanguageManager.getText("ENGLISH", answer.answerValue)
println("Entry " + entry)
println("Question " + questionKey)
println("Answer " + answer.answerValue)
val answerText = rawAnswerText.trim().removePrefix("[").removeSuffix("]")
yPosition = drawMultilineText(canvas, "Question: $questionText", 20f, yPosition, paint, pageWidth - 40, isBold = true)
yPosition += 8f
yPosition = drawMultilineText(canvas, "Answer: $answerText", 20f, yPosition, paint, pageWidth - 40)
yPosition += 20f
paint.strokeWidth = 0.5f
canvas.drawLine(20f, yPosition - 30f, pageWidth - 20f, yPosition - 30f, paint)
paint.strokeWidth = 0f
val sanitizedQuestion = questionText.replace(",", " ").replace("\n", " ")
val sanitizedAnswer = answerText.replace(",", " ").replace("\n", " ")
csvBuilder.appendLine("${actualClientCode},${entry.questionnaireId},${entry.isDone},${entry.sumPoints ?: ""},\"$sanitizedQuestion\",\"$sanitizedAnswer\"")
if (yPosition > pageHeight - 60) {
pdfDocument.finishPage(page)
val newPageInfo = PdfDocument.PageInfo.Builder(pageWidth, pageHeight, pdfDocument.pages.size + 1).create()
page = pdfDocument.startPage(newPageInfo)
canvas = page.canvas
yPosition = 40f
}
}
pdfDocument.finishPage(page)
}
Log.d("CSV_OUTPUT", "Generated CSV:\n${csvBuilder.toString()}")
val pdfFileName = "DatabaseOutput_${actualClientCode}.pdf"
val csvFileName = "DatabaseOutput_${actualClientCode}.csv"
val resolver = activity.contentResolver
val deleteIfExists: (String) -> Unit = { name ->
val projection = arrayOf(android.provider.MediaStore.MediaColumns._ID)
val selection = "${android.provider.MediaStore.MediaColumns.DISPLAY_NAME} = ?"
val selectionArgs = arrayOf(name)
val query = resolver.query(android.provider.MediaStore.Downloads.EXTERNAL_CONTENT_URI, projection, selection, selectionArgs, null)
query?.use { cursor ->
if (cursor.moveToFirst()) {
val idColumn = cursor.getColumnIndexOrThrow(android.provider.MediaStore.MediaColumns._ID)
val id = cursor.getLong(idColumn)
val deleteUri = android.content.ContentUris.withAppendedId(android.provider.MediaStore.Downloads.EXTERNAL_CONTENT_URI, id)
resolver.delete(deleteUri, null, null)
}
}
}
deleteIfExists(pdfFileName)
deleteIfExists(csvFileName)
try {
val pdfUri = resolver.insert(
android.provider.MediaStore.Downloads.EXTERNAL_CONTENT_URI,
android.content.ContentValues().apply {
put(android.provider.MediaStore.MediaColumns.DISPLAY_NAME, pdfFileName)
put(android.provider.MediaStore.MediaColumns.MIME_TYPE, "application/pdf")
put(android.provider.MediaStore.MediaColumns.RELATIVE_PATH, "Download/")
}
)
val csvUri = resolver.insert(
android.provider.MediaStore.Downloads.EXTERNAL_CONTENT_URI,
android.content.ContentValues().apply {
put(android.provider.MediaStore.MediaColumns.DISPLAY_NAME, csvFileName)
put(android.provider.MediaStore.MediaColumns.MIME_TYPE, "text/csv")
put(android.provider.MediaStore.MediaColumns.RELATIVE_PATH, "Download/")
}
)
pdfUri?.let {
resolver.openOutputStream(it)?.use { out -> pdfDocument.writeTo(out) }
pdfDocument.close()
}
csvUri?.let {
resolver.openOutputStream(it)?.use { out ->
out.write(csvBuilder.toString().toByteArray(Charsets.UTF_8))
}
}
withContext(Dispatchers.Main) {
Toast.makeText(activity, "PDF und CSV gespeichert in Downloads", Toast.LENGTH_LONG).show()
pdfUri?.let {
val intent = android.content.Intent(android.content.Intent.ACTION_VIEW).apply {
setDataAndType(it, "application/pdf")
addFlags(android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION or android.content.Intent.FLAG_ACTIVITY_NO_HISTORY)
}
try {
activity.startActivity(intent)
} catch (e: android.content.ActivityNotFoundException) {
Toast.makeText(activity, "Kein PDF-Viewer installiert", Toast.LENGTH_SHORT).show()
}
}
}
} catch (e: Exception) {
Log.e("SAVE", "Fehler beim Speichern der Dateien", e)
withContext(Dispatchers.Main) {
Toast.makeText(activity, "Fehler beim Speichern: ${e.message}", Toast.LENGTH_LONG).show()
}
}
}
}
private fun drawMultilineText(
canvas: Canvas,
text: String,
x: Float,
yStart: Float,
paint: Paint,
maxWidth: Int,
isBold: Boolean = false
): Float {
paint.isFakeBoldText = isBold
val words = text.split(" ")
var line = ""
var y = yStart
for (word in words) {
val testLine = if (line.isEmpty()) word else "$line $word"
val lineWidth = paint.measureText(testLine)
if (lineWidth > maxWidth) {
canvas.drawText(line, x, y, paint)
y += paint.textSize * 1.4f
line = word
} else {
line = testLine
}
}
if (line.isNotEmpty()) {
canvas.drawText(line, x, y, paint)
y += paint.textSize * 1.4f
}
paint.isFakeBoldText = false
return y
}
private fun setupSaveButton() {
saveButton.text = LanguageManager.getText(languageID, "save")
saveButton.setOnClickListener {
val clientCode = editText.text.toString().trim()
if (clientCode.isBlank()) {
val message = LanguageManager.getText(languageID, "please_client_code")
Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
GlobalValues.LAST_CLIENT_CODE = clientCode
showCompletedQuestionnaires(clientCode)
}
}
private fun setupEditButton() {
editButton.text = LanguageManager.getText(languageID, "edit")
editButton.setOnClickListener {
val clientCode = editText.text.toString().trim()
if (clientCode.isBlank()) {
val message = LanguageManager.getText(languageID, "please_client_code")
Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
GlobalValues.LAST_CLIENT_CODE = clientCode
CoroutineScope(Dispatchers.IO).launch {
val completedEntries = MyApp.database.completedQuestionnaireDao().getAllForClient(clientCode)
val completedFiles = completedEntries
.filter { it.isDone }
.map { it.questionnaireId.lowercase() }
buttonPoints.clear()
for (entry in completedEntries) {
if (entry.isDone) {
buttonPoints[entry.questionnaireId] = entry.sumPoints ?: 0
}
}
withContext(Dispatchers.Main) {
updateButtonTexts()
val enabledButtons = questionnaireFiles.filter { (_, fileName) ->
completedFiles.any { completedId -> fileName.lowercase().contains(completedId) }
}.keys.toList()
setButtonsEnabled(enabledButtons)
}
}
}
}
private fun setupUploadButton() {
uploadButton.text = "Upload"
uploadButton.setOnClickListener {
val clientCode = editText.text.toString().trim()
GlobalValues.LAST_CLIENT_CODE = clientCode
Toast.makeText(activity, "Datenbank wird hochgeladen...", Toast.LENGTH_SHORT).show()
DatabaseUploader.uploadDatabase(activity)
}
}
// --- Füge diese Funktion in deine Klasse ein ---
private fun isDatabasePopulated(): Boolean {
return try {
// Wir prüfen, ob die Datenbank mindestens eine nicht-interne Tabelle enthält.
// Das ist robust gegenüber verschiedenen Tabellennamen.
val db = MyApp.database.openHelper.readableDatabase
val cursor = db.query(
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name != 'room_master_table'"
)
cursor.use { it.count > 0 }
} catch (e: Exception) {
// Falls etwas schiefgeht (z.B. DB noch nicht vorhanden), gilt: nicht vorhanden
false
}
}
private fun setupDownloadButton() {
downloadButton.text = "Download"
downloadButton.setOnClickListener {
val clientCode = editText.text.toString().trim()
GlobalValues.LAST_CLIENT_CODE = clientCode
Toast.makeText(activity, "Datenbank wird heruntergeladen...", Toast.LENGTH_SHORT).show()
DatabaseDownloader.downloadAndReplaceDatabase(activity)
updateMainButtonsState(true)
}
}
private fun updateMainButtonsState(isDatabaseAvailable: Boolean) {
val buttons = listOf(buttonLoad, saveButton, editButton)
buttons.forEach { button ->
button.isEnabled = isDatabaseAvailable
button.alpha = if (isDatabaseAvailable) 1.0f else 0.5f
}
}
}

View File

@ -1,7 +1,23 @@
package com.dano.test1 package com.dano.test1
import android.content.res.Configuration
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Bundle import android.os.Bundle
import android.view.View
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.dano.test1.network.DatabaseDownloader
import com.dano.test1.network.LoginManager
import com.dano.test1.network.TokenStore
import com.dano.test1.questionnaire.QuestionnaireBase
import com.dano.test1.ui.HandlerOpeningScreen
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@ -10,18 +26,189 @@ class MainActivity : AppCompatActivity() {
var isInQuestionnaire: Boolean = false var isInQuestionnaire: Boolean = false
var isFirstQuestionnairePage: Boolean = false var isFirstQuestionnairePage: Boolean = false
private var progress: ProgressBar? = null
// LIVE: Network-Callback (optional für Statusleiste)
private var netCb: ConnectivityManager.NetworkCallback? = null
// Wir kennen hier (vor dem OpeningScreen) noch keine Nutzerwahl → Deutsch als Startsprache.
private val bootLanguageId: String get() = "GERMAN"
private fun t(key: String): String = LanguageManager.getText(bootLanguageId, key)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// Initialize the opening screen handler and show the opening screen
// === Offline-Start ermöglichen ===
// Bedingung: gespeicherter User/Token UND lokale DB vorhanden -> direkt OpeningScreen
val hasCreds = !TokenStore.getUsername(this).isNullOrBlank() && !TokenStore.getToken(this).isNullOrBlank()
val hasDb = hasLocalDb()
if (hasCreds && hasDb) {
openingScreenHandler = HandlerOpeningScreen(this) openingScreenHandler = HandlerOpeningScreen(this)
openingScreenHandler.init() openingScreenHandler.init()
return
}
// Sonst: Login-Dialog -> Login -> DB (einmalig) laden -> OpeningScreen
showLoginThenDownload()
}
/** Prüft, ob die lokale DB-Datei vorhanden ist. */
private fun hasLocalDb(): Boolean {
val dbFile = getDatabasePath("questionnaire_database")
return dbFile != null && dbFile.exists() && dbFile.length() > 0
}
/** Zeigt den Login-Dialog an, führt Login aus und lädt danach einmalig die DB. */
private fun showLoginThenDownload() {
val container = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
setPadding(dp(20), dp(8), dp(20), 0)
}
val etUser = EditText(this).apply {
hint = t("username_hint")
setSingleLine()
}
val etPass = EditText(this).apply {
hint = t("password_hint")
setSingleLine()
inputType = android.text.InputType.TYPE_CLASS_TEXT or
android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD
}
container.addView(etUser)
container.addView(etPass)
val dialog = AlertDialog.Builder(this)
.setTitle(t("login_required_title"))
.setView(container)
.setCancelable(false)
.setPositiveButton(t("login_btn")) { _, _ ->
val user = etUser.text.toString().trim()
val pass = etPass.text.toString()
if (user.isEmpty() || pass.isEmpty()) {
Toast.makeText(this, t("please_username_password"), Toast.LENGTH_SHORT).show()
showLoginThenDownload()
return@setPositiveButton
}
showBusy(true)
LoginManager.loginUserWithCredentials(
context = this,
username = user,
password = pass,
onSuccess = { token ->
// Nach erfolgreichem Login: einmalig komplette DB ziehen
DatabaseDownloader.downloadAndReplaceDatabase(
context = this,
token = token
) { ok ->
showBusy(false)
// Wenn Download fehlgeschlagen ist, aber evtl. schon eine DB lokal liegt,
// lassen wir den Nutzer trotzdem weiterarbeiten (Offline).
if (!ok && !hasLocalDb()) {
Toast.makeText(this, t("download_failed_no_local_db"), Toast.LENGTH_LONG).show()
// Zurück zum Login, damit man es erneut probieren kann
showLoginThenDownload()
return@downloadAndReplaceDatabase
}
if (!ok) {
Toast.makeText(this, t("download_failed_use_offline"), Toast.LENGTH_LONG).show()
}
// Opening-Screen starten
openingScreenHandler = HandlerOpeningScreen(this)
openingScreenHandler.init()
openingScreenHandler.refreshHeaderStatusLive()
}
},
onError = { msg ->
showBusy(false)
val txt = t("login_failed_with_reason").replace("{reason}", msg ?: "")
Toast.makeText(this, txt, Toast.LENGTH_LONG).show()
showLoginThenDownload()
}
)
}
.setNegativeButton(t("exit_btn")) { _, _ -> finishAffinity() }
.create()
dialog.show()
}
private fun showBusy(show: Boolean) {
if (show) {
if (progress == null) {
progress = ProgressBar(this).apply {
isIndeterminate = true
(window?.decorView as? android.view.ViewGroup)?.addView(this)
}
}
progress?.visibility = View.VISIBLE
} else {
progress?.visibility = View.GONE
}
}
private fun dp(v: Int): Int = (v * resources.displayMetrics.density).toInt()
// --- LIVE NETZSTATUS (optional, für deine Status-Leiste) ---
override fun onResume() {
super.onResume()
registerNetworkCallback()
if (::openingScreenHandler.isInitialized && !isInQuestionnaire) {
openingScreenHandler.refreshHeaderStatusLive()
}
}
override fun onPause() {
super.onPause()
unregisterNetworkCallback()
}
private fun registerNetworkCallback() {
if (netCb != null) return
val cm = getSystemService(ConnectivityManager::class.java)
val req = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build()
netCb = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
runOnUiThread {
if (::openingScreenHandler.isInitialized && !isInQuestionnaire) {
openingScreenHandler.refreshHeaderStatusLive()
}
}
}
override fun onLost(network: Network) {
runOnUiThread {
if (::openingScreenHandler.isInitialized && !isInQuestionnaire) {
openingScreenHandler.refreshHeaderStatusLive()
}
}
}
override fun onCapabilitiesChanged(network: Network, caps: NetworkCapabilities) {
runOnUiThread {
if (::openingScreenHandler.isInitialized && !isInQuestionnaire) {
openingScreenHandler.refreshHeaderStatusLive()
}
}
}
}
cm.registerNetworkCallback(req, netCb!!)
}
private fun unregisterNetworkCallback() {
val cb = netCb ?: return
val cm = getSystemService(ConnectivityManager::class.java)
cm.unregisterNetworkCallback(cb)
netCb = null
}
// --- /LIVE NETZSTATUS ---
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
} }
/**
* Starts the given questionnaire and attaches it to this activity.
* @param questionnaire The questionnaire instance to start.
* @param languageID The language identifier for localization.
*/
fun startQuestionnaire(questionnaire: QuestionnaireBase<*>, languageID: String) { fun startQuestionnaire(questionnaire: QuestionnaireBase<*>, languageID: String) {
isInQuestionnaire = true isInQuestionnaire = true
isFirstQuestionnairePage = true isFirstQuestionnairePage = true
@ -29,24 +216,17 @@ class MainActivity : AppCompatActivity() {
questionnaire.startQuestionnaire() questionnaire.startQuestionnaire()
} }
/**
* Handle the back button press.
* If the openingScreenHandler can handle it, do not call super.
* Otherwise, call the default back press behavior.
*/
override fun onBackPressed() { override fun onBackPressed() {
if (!openingScreenHandler.onBackPressed()) { if (!::openingScreenHandler.isInitialized || !openingScreenHandler.onBackPressed()) {
super.onBackPressed() super.onBackPressed()
} }
} }
/**
* Finish the questionnaire and return to the opening screen.
*/
fun finishQuestionnaire() { fun finishQuestionnaire() {
// For example, switch back to the opening screen:
isInQuestionnaire = false isInQuestionnaire = false
isFirstQuestionnairePage = false isFirstQuestionnairePage = false
if (::openingScreenHandler.isInitialized) {
openingScreenHandler.init() openingScreenHandler.init()
} }
}
} }

View File

@ -6,6 +6,19 @@ import androidx.room.Room
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import com.dano.test1.data.AppDatabase import com.dano.test1.data.AppDatabase
/*
MyApp (Application)
- Einstiegspunkt der App, der einmal pro Prozessstart initialisiert wird.
Besonderheiten der DB-Konfiguration:
- Name: "questionnaire_database"
- fallbackToDestructiveMigration():
* Falls sich das Schema ändert und keine Migration vorliegt,wird die DB zerstört und neu angelegt.
- setJournalMode(TRUNCATE):
* Verwendet TRUNCATE-Journal (keine separaten -wal/-shm Dateien), es existiert nur die Hauptdatei „questionnaire_database“.
- Callback onOpen():
* Loggt beim Öffnen der Datenbank einen Hinweis.
*/
class MyApp : Application() { class MyApp : Application() {
companion object { companion object {
@ -16,7 +29,7 @@ class MyApp : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
// Room Datenbank bauen: nur die Hauptdatei, ohne WAL und Journal // Room-Datenbank bauen: nur die Hauptdatei, ohne WAL und Journal
database = Room.databaseBuilder( database = Room.databaseBuilder(
applicationContext, applicationContext,
AppDatabase::class.java, AppDatabase::class.java,

View File

@ -3,6 +3,18 @@ package com.dano.test1.data
import androidx.room.Database import androidx.room.Database
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
/*
Zentrale Room-Datenbank der App. Diese Klasse beschreibt:
- welche Tabellen (entities) es gibt: Client, Questionnaire, Question, Answer, CompletedQuestionnaire
- die Datenbank-Version (version = 1) für Migrations/Schema-Updates
Über die abstrakten DAO-Getter (clientDao(), questionnaireDao(), ) erhält der Rest der App Typsichere Zugriffe auf die jeweiligen Tabellen.
Hinweis:
- Room erzeugt zur Build-Zeit die konkrete Implementierung dieser abstrakten Klasse.
- Eine Instanz der Datenbank wird typischerweise per Room.databaseBuilder(...) erstellt und als Singleton verwendet.
*/
@Database( @Database(
entities = [ entities = [
Client::class, Client::class,

View File

@ -2,8 +2,18 @@ package com.dano.test1.data
import androidx.room.* import androidx.room.*
/*
Data-Access-Objekte (DAOs) für die Room-Datenbank.
Sie kapseln alle typsicheren Lese-/Schreiboperationen für die Tabellen clients, questionnaires, questions, answers und completed_questionnaires.
Hinweis:
- Die konkreten Implementierungen erzeugt Room zur Build-Zeit.
- DAOs werden über die AppDatabase (Room.databaseBuilder(...)) bezogen.
*/
@Dao @Dao
interface ClientDao { interface ClientDao {
@Insert(onConflict = OnConflictStrategy.IGNORE) @Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertClient(client: Client) suspend fun insertClient(client: Client)
@ -20,18 +30,22 @@ interface ClientDao {
suspend fun getAllClients(): List<Client> suspend fun getAllClients(): List<Client>
} }
@Dao @Dao
interface QuestionnaireDao { interface QuestionnaireDao {
@Insert(onConflict = OnConflictStrategy.IGNORE) @Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertQuestionnaire(questionnaire: Questionnaire) suspend fun insertQuestionnaire(questionnaire: Questionnaire)
@Query("SELECT * FROM questionnaires WHERE id = :id LIMIT 1") @Query("SELECT * FROM questionnaires WHERE id = :id LIMIT 1")
suspend fun getById(id: String): Questionnaire? suspend fun getById(id: String): Questionnaire?
@Query("SELECT * FROM questionnaires")
suspend fun getAll(): List<Questionnaire>
} }
@Dao @Dao
interface QuestionDao { interface QuestionDao {
@Insert(onConflict = OnConflictStrategy.IGNORE) @Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertQuestions(questions: List<Question>) suspend fun insertQuestions(questions: List<Question>)
@ -45,8 +59,10 @@ interface QuestionDao {
suspend fun getQuestionsForQuestionnaire(questionnaireId: String): List<Question> suspend fun getQuestionsForQuestionnaire(questionnaireId: String): List<Question>
} }
@Dao @Dao
interface AnswerDao { interface AnswerDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAnswers(answers: List<Answer>) suspend fun insertAnswers(answers: List<Answer>)
@ -63,10 +79,22 @@ interface AnswerDao {
@Query("SELECT * FROM answers WHERE clientCode = :clientCode") @Query("SELECT * FROM answers WHERE clientCode = :clientCode")
suspend fun getAnswersForClient(clientCode: String): List<Answer> suspend fun getAnswersForClient(clientCode: String): List<Answer>
@Query("""
DELETE FROM answers
WHERE clientCode = :clientCode
AND questionId IN (
SELECT questionId FROM questions WHERE questionnaireId = :questionnaireId
)
""")
suspend fun deleteAnswersForClientAndQuestionnaire(
clientCode: String,
questionnaireId: String
)
} }
@Dao @Dao
interface CompletedQuestionnaireDao { interface CompletedQuestionnaireDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(entry: CompletedQuestionnaire) suspend fun insert(entry: CompletedQuestionnaire)

View File

@ -2,16 +2,41 @@ package com.dano.test1.data
import androidx.room.* import androidx.room.*
/*
Room-Entities (Tabellen) der App.
- Definieren das Schema für Clients, Questionnaires, Questions, Answers und CompletedQuestionnaires.
- Beziehungen:
* Question -> Questionnaire (FK, CASCADE)
* Answer -> Client (FK, CASCADE)
* Answer -> Question (FK, CASCADE)
* CompletedQuestionnaire -> Client (FK, CASCADE)
* CompletedQuestionnaire -> Questionnaire (FK, CASCADE)
- Primärschlüssel:
* Client: clientCode
* Questionnaire: id
* Question: questionId
* Answer: (clientCode, questionId) eine Antwort je Client & Frage
* CompletedQuestionnaire: (clientCode, questionnaireId) ein Status je Client & Fragebogen
*/
/* Tabelle: clients Eindeutige Identifikation eines Clients per clientCode. */
@Entity(tableName = "clients") @Entity(tableName = "clients")
data class Client( data class Client(
@PrimaryKey val clientCode: String, @PrimaryKey val clientCode: String,
) )
/* Tabelle: questionnaires Eindeutige Fragebogen-IDs. */
@Entity(tableName = "questionnaires") @Entity(tableName = "questionnaires")
data class Questionnaire( data class Questionnaire(
@PrimaryKey val id: String, @PrimaryKey val id: String,
) )
/*
Tabelle: questions
- Jede Frage gehört zu genau einem Fragebogen (questionnaireId).
- Fremdschlüssel sorgt dafür, dass beim Löschen eines Fragebogens die zugehörigen Fragen mit gelöscht werden.
- Index auf questionnaireId beschleunigt Abfragen alle Fragen eines Fragebogens.
*/
@Entity( @Entity(
tableName = "questions", tableName = "questions",
foreignKeys = [ foreignKeys = [
@ -30,6 +55,12 @@ data class Question(
val question: String = "" val question: String = ""
) )
/*
Tabelle: answers
- Zusammengesetzter Primärschlüssel (clientCode, questionId):
* Pro Client und Frage existiert höchstens eine Antwort.
* Löscht man den Client oder die Frage, werden die zugehörigen Antworten mit entfernt.
*/
@Entity( @Entity(
tableName = "answers", tableName = "answers",
primaryKeys = ["clientCode", "questionId"], primaryKeys = ["clientCode", "questionId"],
@ -55,6 +86,17 @@ data class Answer(
val answerValue: String = "" val answerValue: String = ""
) )
/*
Tabelle: completed_questionnaires
- Zusammengesetzter Primärschlüssel (clientCode, questionnaireId):
* Hält den Abschluss-Status eines Fragebogens pro Client.
- FKs mit CASCADE:
* Beim Löschen eines Clients oder Fragebogens verschwindet der Status-Eintrag ebenfalls.
- Indizes auf clientCode und questionnaireId für schnelle Lookups.
- timestamp: Zeitpunkt der Statusänderung (Default: now).
- isDone: true/false abgeschlossen oder nicht.
- sumPoints: optionaler Score des Fragebogens.
*/
@Entity( @Entity(
tableName = "completed_questionnaires", tableName = "completed_questionnaires",
primaryKeys = ["clientCode", "questionnaireId"], primaryKeys = ["clientCode", "questionnaireId"],
@ -81,4 +123,3 @@ data class CompletedQuestionnaire(
val isDone: Boolean, val isDone: Boolean,
val sumPoints: Int? = null val sumPoints: Int? = null
) )

View File

@ -0,0 +1,182 @@
package com.dano.test1.data
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 com.dano.test1.LanguageManager
import com.dano.test1.MyApp
import org.apache.poi.ss.usermodel.Row
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import java.io.ByteArrayOutputStream
import java.io.File
/*
Aufgabe:
- Baut eine Excel-Datei (XLSX) mit allen Clients als Zeilen und einem konfigurierbaren Spalten-Layout.
- Speichert die Datei ausschließlich in den öffentlichen „Downloads“-Ordner
Datenquelle:
- Liest die Spaltenreihenfolge/Spalten-IDs über HeaderOrderRepository.loadOrderedIds().
- Holt alle Clients, Fragebogen-IDs sowie Antworten aus der lokalen Room-Datenbank
Ausgabeformat (Sheet „Headers“):
- Zeile 1: Spalten-IDs (erste Zelle „#“ für laufende Nummer).
- Zeile 2: Englische Beschriftung/Fragetext je Spalte (ermittelt via englishQuestionForId + LanguageManager).
- Ab Zeile 3: Pro Client eine Datenzeile.
* Für Spalten-ID „client_code“: der Client-Code.
* Für Spalten-IDs, die einem Fragebogen entsprechen (Questionnaire-ID): „Done“/„Not Done“ (Abschlussstatus).
* Für sonstige Spalten-IDs (Antwort-IDs): Antwortwert oder „None“, falls leer.
*/
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 = ByteArrayOutputStream().use { bos ->
wb.write(bos); bos.toByteArray()
}
wb.close()
return saveToDownloads(
filename = "ClientHeaders.xlsx",
mimeType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
bytes = bytes
)
}
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 = File(downloadsDir, filename)
outFile.writeBytes(bytes)
MediaScannerConnection.scanFile(
context,
arrayOf(outFile.absolutePath),
arrayOf(mimeType),
null
)
Uri.fromFile(outFile)
}
}
private suspend fun englishQuestionForId(id: String, questionnaireIdSet: Set<String>): 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
}
}

View File

@ -0,0 +1,90 @@
package com.dano.test1.data
import android.content.Context
import android.util.Log
import android.widget.Toast
import com.dano.test1.LanguageManager
import org.apache.poi.ss.usermodel.CellType
import org.apache.poi.ss.usermodel.DateUtil
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.json.JSONArray
import java.nio.charset.Charset
/*
Zweck:
- Liefert die Reihenfolge/IDs der zu exportierenden Header (Spalten) für den Excel-Export.
- Bevorzugte Quelle ist eine Excel-Datei aus den App-Assets („header_order.xlsx“), als Fallback wird eine JSON-Datei („header_order.json“) genutzt.
*/
class HeaderOrderRepository(
private val context: Context,
// Sprache abrufen (Standard: Deutsch, damit es ohne OpeningScreen schon sinnvoll ist)
private val languageIDProvider: () -> String = { "GERMAN" }
) {
private val tag = "HeaderOrderRepository"
private var orderedIdsCache: List<String>? = null
private fun t(key: String): String = LanguageManager.getText(languageIDProvider(), key)
fun loadOrderedIds(): List<String> {
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() }
if (fromXlsx.isNotEmpty()) {
orderedIdsCache = fromXlsx
return fromXlsx
}
return try {
val stream = context.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(context, t("no_header_template_found"), Toast.LENGTH_LONG).show()
emptyList()
}
}
private fun loadOrderedIdsFromExcel(assetFileName: String): List<String> {
context.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<String>()
for (i in first until last) {
val cell = row.getCell(i) ?: continue
val value = when (cell.cellType) {
CellType.STRING -> cell.stringCellValue
CellType.NUMERIC ->
if (DateUtil.isCellDateFormatted(cell))
cell.dateCellValue.time.toString()
else {
val n = cell.numericCellValue
if (n % 1.0 == 0.0) n.toLong().toString() else n.toString()
}
CellType.BOOLEAN -> cell.booleanCellValue.toString()
CellType.FORMULA -> cell.richStringCellValue.string
else -> ""
}.trim()
if (value.isEmpty()) continue
if (i == first && value == "#") continue // „#“ in Spalte 0 ignorieren
out.add(value)
}
return out
}
}
}
}

View File

@ -0,0 +1,63 @@
package com.dano.test1.network
import android.content.Context
import android.util.Log
import com.dano.test1.AES256Helper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.File
import java.io.FileOutputStream
object DatabaseDownloader {
private const val DB_NAME = "questionnaire_database"
private const val SERVER_DOWNLOAD_URL = "https://daniel-ocks.de/qdb/downloadFull.php"
private val client = OkHttpClient()
// Neue Variante mit Callback
fun downloadAndReplaceDatabase(context: Context, token: String, onDone: ((Boolean) -> Unit)? = null) {
CoroutineScope(Dispatchers.IO).launch {
var ok = false
try {
val request = Request.Builder()
.url(SERVER_DOWNLOAD_URL)
.header("Authorization", "Bearer $token")
.build()
val response = client.newCall(request).execute()
if (!response.isSuccessful) {
Log.e("DOWNLOAD", "HTTP ${response.code}")
withContext(Dispatchers.Main) { onDone?.invoke(false) }
return@launch
}
val encFile = File(context.cacheDir, "downloaded_database.enc")
response.body?.byteStream()?.use { input ->
FileOutputStream(encFile).use { output -> input.copyTo(output) }
}
val decryptedBytes = AES256Helper.decryptFileWithToken(encFile, token)
val dbFile = context.getDatabasePath(DB_NAME)
if (dbFile.exists()) dbFile.delete()
FileOutputStream(dbFile).use { it.write(decryptedBytes) }
Log.d("DOWNLOAD", "DB erfolgreich ersetzt")
ok = true
} catch (e: Exception) {
Log.e("DOWNLOAD", "Fehler", e)
} finally {
withContext(Dispatchers.Main) { onDone?.invoke(ok) }
}
}
}
// Abwärtskompatible alte Signatur
fun downloadAndReplaceDatabase(context: Context, token: String) {
downloadAndReplaceDatabase(context, token, null)
}
}

View File

@ -0,0 +1,166 @@
package com.dano.test1.network
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.util.Base64
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import android.database.Cursor
import com.dano.test1.AES256Helper
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.asRequestBody
import org.json.JSONArray
import org.json.JSONObject
import java.io.File
import java.io.IOException
import kotlin.system.exitProcess
object DatabaseUploader {
private const val DB_NAME = "questionnaire_database"
private const val SERVER_DELTA_URL = "https://daniel-ocks.de/qdb/uploadDeltaTest5.php"
private const val SERVER_CHECK_URL = "https://daniel-ocks.de/qdb/checkDatabaseExists.php"
private val client = OkHttpClient()
private fun uploadDatabase(context: Context, token: String) {
CoroutineScope(Dispatchers.IO).launch {
try {
val dbFile = context.getDatabasePath(DB_NAME)
if (!dbFile.exists()) {
Log.e("UPLOAD", "DB fehlt: ${dbFile.absolutePath}")
return@launch
}
// WAL sauber schließen (falls aktiv)
try {
val db = SQLiteDatabase.openDatabase(dbFile.absolutePath, null, SQLiteDatabase.OPEN_READWRITE)
db.rawQuery("PRAGMA wal_checkpoint(FULL);", null).use { /* noop */ }
db.close()
} catch (_: Exception) { }
checkDatabaseExists()
uploadPseudoDelta(context, dbFile, token)
} catch (e: Exception) {
Log.e("UPLOAD", "Fehler", e)
}
}
}
private fun checkDatabaseExists(): Boolean {
return try {
val req = Request.Builder().url(SERVER_CHECK_URL).get().build()
client.newCall(req).execute().use { resp ->
if (!resp.isSuccessful) return false
val body = resp.body?.string() ?: return false
try { JSONObject(body).optBoolean("exists", false) } catch (_: Exception) { false }
}
} catch (e: Exception) { false }
}
private fun uploadPseudoDelta(context: Context, file: File, token: String) {
try {
val db = SQLiteDatabase.openDatabase(file.absolutePath, null, SQLiteDatabase.OPEN_READONLY)
val data = JSONObject().apply {
put("clients", queryToJsonArray(db, "SELECT clientCode FROM clients"))
put("questionnaires", queryToJsonArray(db, "SELECT id FROM questionnaires"))
put("questions", queryToJsonArray(db, "SELECT questionId, questionnaireId, question FROM questions"))
put("answers", queryToJsonArray(db, "SELECT clientCode, questionId, answerValue FROM answers"))
put(
"completed_questionnaires",
queryToJsonArray(db, "SELECT clientCode, questionnaireId, timestamp, isDone, sumPoints FROM completed_questionnaires")
)
}
db.close()
// JSON -> verschlüsselte Payload
val tmpJson = File(context.cacheDir, "payload.json").apply { writeText(data.toString()) }
val tmpEnc = File(context.cacheDir, "payload.enc")
try {
AES256Helper.encryptFileWithToken(tmpJson, tmpEnc, token)
} catch (e: Exception) {
Log.e("UPLOAD", "Verschlüsselung fehlgeschlagen", e)
tmpJson.delete(); return
}
val body = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("token", token) // bleibt für Kompatibilität enthalten
.addFormDataPart("file", "payload.enc", tmpEnc.asRequestBody("application/octet-stream".toMediaType()))
.build()
// WICHTIG: Jetzt HTTPS + Konstanten-URL verwenden, plus Bearer-Header
val request = Request.Builder()
.url(SERVER_DELTA_URL)
.post(body)
.header("Authorization", "Bearer $token")
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.e("UPLOAD", "Fehlgeschlagen: ${e.message}")
tmpJson.delete(); tmpEnc.delete()
}
override fun onResponse(call: Call, response: Response) {
val respBody = try { response.body?.string() ?: "" } catch (_: Exception) { "" }
if (response.isSuccessful) {
Log.d("UPLOAD", "OK: $respBody")
// alte Logik: lokale DB + Neben­dateien löschen
try {
if (!file.delete()) Log.w("UPLOAD", "Lokale DB nicht gelöscht.")
File(file.parent, "${file.name}-journal").delete()
File(file.parent, "${file.name}-wal").delete()
File(file.parent, "${file.name}-shm").delete()
} catch (e: Exception) {
Log.w("UPLOAD", "Fehler beim Löschen lokaler DB-Dateien", e)
}
} else {
Log.e("UPLOAD", "HTTP ${response.code}: $respBody")
}
tmpJson.delete(); tmpEnc.delete()
// unverändert beibehalten
try { exitProcess(0) } catch (_: Exception) {}
}
})
} catch (e: Exception) {
Log.e("UPLOAD", "Exception", e)
}
}
private fun queryToJsonArray(db: SQLiteDatabase, query: String): JSONArray {
val c = db.rawQuery(query, null)
val arr = JSONArray()
c.use {
val cols = it.columnNames
while (it.moveToNext()) {
val obj = JSONObject()
for (col in cols) {
val idx = it.getColumnIndex(col)
if (idx >= 0) {
when (it.getType(idx)) {
Cursor.FIELD_TYPE_INTEGER -> obj.put(col, it.getLong(idx))
Cursor.FIELD_TYPE_FLOAT -> obj.put(col, it.getDouble(idx))
Cursor.FIELD_TYPE_STRING -> obj.put(col, it.getString(idx))
Cursor.FIELD_TYPE_NULL -> obj.put(col, JSONObject.NULL)
Cursor.FIELD_TYPE_BLOB -> obj.put(col, Base64.encodeToString(it.getBlob(idx), Base64.NO_WRAP))
else -> obj.put(col, it.getString(idx))
}
}
}
arr.put(obj)
}
}
return arr
}
fun uploadDatabaseWithToken(context: Context, token: String) {
uploadDatabase(context, token)
}
}

View File

@ -0,0 +1,218 @@
package com.dano.test1.network
import android.app.AlertDialog
import android.content.Context
import android.text.InputType
import android.util.Log
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.Toast
import kotlinx.coroutines.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
object LoginManager {
private const val SERVER_LOGIN_URL = "https://daniel-ocks.de/qdb/login.php"
private const val SERVER_CHANGE_URL = "https://daniel-ocks.de/qdb/change_password.php"
private val client = OkHttpClient()
fun loginUserWithCredentials(
context: Context,
username: String,
password: String,
onSuccess: (String) -> Unit,
onError: (String) -> Unit
) {
CoroutineScope(Dispatchers.IO).launch {
try {
val bodyJson = JSONObject()
.put("username", username)
.put("password", password)
.toString()
.toRequestBody("application/json".toMediaType())
val request = Request.Builder()
.url(SERVER_LOGIN_URL)
.post(bodyJson)
.build()
val response = client.newCall(request).execute()
val text = response.body?.string()
if (!response.isSuccessful || text == null) {
withContext(Dispatchers.Main) { onError("Fehler beim Login (${response.code})") }
return@launch
}
val json = JSONObject(text)
if (!json.optBoolean("success")) {
withContext(Dispatchers.Main) { onError(json.optString("message", "Login fehlgeschlagen")) }
return@launch
}
// Passwortwechsel erforderlich?
if (json.optBoolean("must_change_password", false)) {
withContext(Dispatchers.Main) {
showChangePasswordDialog(
context = context,
username = username,
oldPassword = password,
onChanged = { token ->
// Nach PW-Änderung direkt eingeloggt
TokenStore.save(context, token, username)
onSuccess(token)
},
onError = onError
)
}
return@launch
}
// normaler Login: Token speichern
val token = json.getString("token")
TokenStore.save(context, token, username)
withContext(Dispatchers.Main) { onSuccess(token) }
} catch (e: Exception) {
Log.e("LOGIN", "Exception", e)
withContext(Dispatchers.Main) { onError("Exception: ${e.message}") }
}
}
}
private fun showChangePasswordDialog(
context: Context,
username: String,
oldPassword: String,
onChanged: (String) -> Unit,
onError: (String) -> Unit
) {
val container = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
setPadding(48, 24, 48, 0)
}
val etNew = EditText(context).apply {
hint = "Neues Passwort"
inputType = InputType.TYPE_CLASS_TEXT or
InputType.TYPE_TEXT_VARIATION_PASSWORD
}
val etRepeat = EditText(context).apply {
hint = "Neues Passwort (wiederholen)"
inputType = InputType.TYPE_CLASS_TEXT or
InputType.TYPE_TEXT_VARIATION_PASSWORD
}
container.addView(etNew)
container.addView(etRepeat)
val dialog = AlertDialog.Builder(context)
.setTitle("Passwort ändern")
.setMessage("Du verwendest ein Standard-Konto. Bitte setze jetzt ein eigenes Passwort.")
.setView(container)
.setPositiveButton("OK", null) // nicht sofort schließen lassen
.setNegativeButton("Abbrechen", null) // nicht sofort schließen lassen
.setCancelable(false)
.create()
dialog.setOnShowListener {
val btnOk = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
val btnCancel = dialog.getButton(AlertDialog.BUTTON_NEGATIVE)
btnOk.setOnClickListener {
etNew.error = null
etRepeat.error = null
val p1 = etNew.text?.toString().orEmpty()
val p2 = etRepeat.text?.toString().orEmpty()
when {
p1.length < 6 -> {
etNew.error = "Mindestens 6 Zeichen."
return@setOnClickListener
}
p1 != p2 -> {
etRepeat.error = "Passwörter stimmen nicht überein."
return@setOnClickListener
}
else -> {
btnOk.isEnabled = false
btnCancel.isEnabled = false
changePassword(
context = context,
username = username,
oldPassword = oldPassword,
newPassword = p1,
onChanged = { token ->
dialog.dismiss()
onChanged(token)
},
onError = { msg ->
btnOk.isEnabled = true
btnCancel.isEnabled = true
Toast.makeText(context, msg, Toast.LENGTH_LONG).show()
}
)
}
}
}
// >>> Überarbeitet: Abbrechen schließt Dialog und informiert den Aufrufer
btnCancel.setOnClickListener {
dialog.dismiss()
onError("Passwortänderung abgebrochen.")
}
}
dialog.show()
}
private fun changePassword(
context: Context,
username: String,
oldPassword: String,
newPassword: String,
onChanged: (String) -> Unit,
onError: (String) -> Unit
) {
CoroutineScope(Dispatchers.IO).launch {
try {
val body = JSONObject()
.put("username", username)
.put("old_password", oldPassword)
.put("new_password", newPassword)
.toString()
.toRequestBody("application/json".toMediaType())
val req = Request.Builder()
.url(SERVER_CHANGE_URL)
.post(body)
.build()
val resp = client.newCall(req).execute()
val txt = resp.body?.string()
if (!resp.isSuccessful || txt == null) {
withContext(Dispatchers.Main) { onError("Fehler beim Ändern (${resp.code})") }
return@launch
}
val json = JSONObject(txt)
if (!json.optBoolean("success")) {
withContext(Dispatchers.Main) { onError(json.optString("message", "Ändern fehlgeschlagen")) }
return@launch
}
val token = json.getString("token")
withContext(Dispatchers.Main) { onChanged(token) }
} catch (e: Exception) {
Log.e("LOGIN", "changePassword Exception", e)
withContext(Dispatchers.Main) { onError("Exception: ${e.message}") }
}
}
}
}

View File

@ -0,0 +1,33 @@
package com.dano.test1.network
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
/*
Zweck:
- Einfache Hilfsklasse, um den aktuellen Online-Status des Geräts zu prüfen.
Funktionsweise:
- `isOnline(context)` nutzt den systemweiten `ConnectivityManager`, fragt die aktive Verbindung (`activeNetwork`) ab und prüft deren `NetworkCapabilities`.
- Es wird nur dann `true` zurückgegeben, wenn:
* eine aktive Verbindung existiert und
* die Verbindung die Fähigkeit „INTERNET“ besitzt und
* die Verbindung als „VALIDATED“ gilt (vom System als funktionsfähig verifiziert).
Verwendung:
- Vo Netzwerkaufrufen (Login, Upload, Download) aufrufen, um „Offline“-Fälle frühzeitig abzufangen und nutzerfreundliche Meldungen zu zeigen.
*/
object NetworkUtils {
fun isOnline(context: Context): Boolean {
return try {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager? ?: return false
val network = cm.activeNetwork ?: return false
val caps = cm.getNetworkCapabilities(network) ?: return false
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
} catch (_: SecurityException) {
false
}
}
}

View File

@ -0,0 +1,46 @@
package com.dano.test1.network
import android.content.Context
/*
TokenStore
- Kleiner Helper zum Verwalten der Login-Sitzung
- Speichert:
* das API-Token (KEY_TOKEN),
* den Usernamen (KEY_USER),
* den Zeitpunkt des Logins in Millisekunden (KEY_LOGIN_TS).
- Bietet Lese-/Schreib-Methoden sowie ein clear(), um alles zurückzusetzen.
Hinweis:
- Die Daten liegen in einer privaten App-Preference-Datei (PREF = "qdb_prefs").
- getLoginTimestamp() eignet sich, um die Token-„Frische“ (Alter) zu berechnen.
*/
object TokenStore {
private const val PREF = "qdb_prefs"
private const val KEY_TOKEN = "token"
private const val KEY_USER = "user"
private const val KEY_LOGIN_TS = "login_ts"
fun save(context: Context, token: String, username: String) {
val now = System.currentTimeMillis()
context.getSharedPreferences(PREF, Context.MODE_PRIVATE)
.edit()
.putString(KEY_TOKEN, token) // API-/Session-Token
.putString(KEY_USER, username) // angemeldeter Benutzername
.putLong(KEY_LOGIN_TS, now) // Zeitpunkt des Logins
.apply()
}
fun getToken(context: Context): String? =
context.getSharedPreferences(PREF, Context.MODE_PRIVATE).getString(KEY_TOKEN, null)
fun getUsername(context: Context): String? =
context.getSharedPreferences(PREF, Context.MODE_PRIVATE).getString(KEY_USER, null)
fun getLoginTimestamp(context: Context): Long =
context.getSharedPreferences(PREF, Context.MODE_PRIVATE).getLong(KEY_LOGIN_TS, 0L)
fun clear(context: Context) {
context.getSharedPreferences(PREF, Context.MODE_PRIVATE).edit().clear().apply()
}
}

View File

@ -1,4 +1,4 @@
package com.dano.test1 package com.dano.test1.questionnaire
import android.view.View import android.view.View
interface QuestionHandler { interface QuestionHandler {

View File

@ -1,10 +1,23 @@
package com.dano.test1 package com.dano.test1.questionnaire
import android.R
import android.app.Activity import android.app.Activity
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import android.widget.* import android.widget.*
import com.dano.test1.LanguageManager
import com.dano.test1.MainActivity
import com.dano.test1.MyApp
import com.dano.test1.data.* import com.dano.test1.data.*
import com.dano.test1.questionnaire.handlers.HandlerClientCoachCode
import com.dano.test1.questionnaire.handlers.HandlerClientNotSigned
import com.dano.test1.questionnaire.handlers.HandlerDateSpinner
import com.dano.test1.questionnaire.handlers.HandlerGlassScaleQuestion
import com.dano.test1.questionnaire.handlers.HandlerLastPage
import com.dano.test1.questionnaire.handlers.HandlerMultiCheckboxQuestion
import com.dano.test1.questionnaire.handlers.HandlerRadioQuestion
import com.dano.test1.questionnaire.handlers.HandlerStringSpinner
import com.dano.test1.questionnaire.handlers.HandlerValueSpinner
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.JsonParser import com.google.gson.JsonParser
import kotlinx.coroutines.* import kotlinx.coroutines.*
@ -61,8 +74,8 @@ abstract class QuestionnaireBase<T> {
} }
protected fun setupSpinner(spinner: Spinner, spinnerValues: List<Any>, selectedValue: Any?) { protected fun setupSpinner(spinner: Spinner, spinnerValues: List<Any>, selectedValue: Any?) {
val adapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, spinnerValues).apply { val adapter = ArrayAdapter(context, R.layout.simple_spinner_item, spinnerValues).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) setDropDownViewResource(R.layout.simple_spinner_dropdown_item)
} }
spinner.adapter = adapter spinner.adapter = adapter
selectedValue?.let { value -> selectedValue?.let { value ->
@ -75,7 +88,7 @@ abstract class QuestionnaireBase<T> {
protected fun navigateTo(layoutResId: Int, setup: (View) -> Unit) { protected fun navigateTo(layoutResId: Int, setup: (View) -> Unit) {
context.setContentView(layoutResId) context.setContentView(layoutResId)
val rootView = context.findViewById<View>(android.R.id.content) val rootView = context.findViewById<View>(R.id.content)
setup(rootView) setup(rootView)
} }
@ -85,7 +98,7 @@ abstract class QuestionnaireBase<T> {
protected fun showEmptyScreen() { protected fun showEmptyScreen() {
navigateTo(getLayoutResId("empty")) { navigateTo(getLayoutResId("empty")) {
setupPrevButton(R.id.Qprev) { goToPreviousQuestion() } setupPrevButton(com.dano.test1.R.id.Qprev) { goToPreviousQuestion() }
} }
} }
@ -155,14 +168,14 @@ abstract class QuestionnaireBase<T> {
protected open fun createHandlerForQuestion(question: QuestionItem): QuestionHandler? { protected open fun createHandlerForQuestion(question: QuestionItem): QuestionHandler? {
return when (question) { return when (question) {
is QuestionItem.RadioQuestion -> HandlerRadioQuestion(context, answers, points, languageID, ::goToNextQuestion, ::goToPreviousQuestion, ::goToQuestionById, ::showToast) is QuestionItem.RadioQuestion -> HandlerRadioQuestion(context, answers, points, languageID, ::goToNextQuestion, ::goToPreviousQuestion, ::goToQuestionById, ::showToast, questionnaireMeta.id)
is QuestionItem.ClientCoachCodeQuestion -> HandlerClientCoachCode(answers, languageID, ::goToNextQuestion, ::goToPreviousQuestion, ::showToast) is QuestionItem.ClientCoachCodeQuestion -> HandlerClientCoachCode(answers, languageID, ::goToNextQuestion, ::goToPreviousQuestion, ::showToast)
is QuestionItem.DateSpinnerQuestion -> HandlerDateSpinner(context, answers, languageID, ::goToNextQuestion, ::goToPreviousQuestion, ::showToast) is QuestionItem.DateSpinnerQuestion -> HandlerDateSpinner(context, answers, languageID, ::goToNextQuestion, ::goToPreviousQuestion, ::showToast, questionnaireMeta.id)
is QuestionItem.ValueSpinnerQuestion -> HandlerValueSpinner(context, answers, languageID, ::goToNextQuestion, ::goToPreviousQuestion, ::goToQuestionById, ::showToast) is QuestionItem.ValueSpinnerQuestion -> HandlerValueSpinner(context, answers, languageID, ::goToNextQuestion, ::goToPreviousQuestion, ::goToQuestionById, ::showToast, questionnaireMeta.id)
is QuestionItem.GlassScaleQuestion -> HandlerGlassScaleQuestion(context, answers, points, languageID, ::goToNextQuestion, ::goToPreviousQuestion, ::showToast) is QuestionItem.GlassScaleQuestion -> HandlerGlassScaleQuestion(context, answers, points, languageID, ::goToNextQuestion, ::goToPreviousQuestion, ::showToast, questionnaireMeta.id)
is QuestionItem.ClientNotSigned -> HandlerClientNotSigned(answers, languageID, ::goToNextQuestion, ::goToPreviousQuestion, ::showToast) is QuestionItem.ClientNotSigned -> HandlerClientNotSigned(answers, languageID, ::goToNextQuestion, ::goToPreviousQuestion, ::showToast)
is QuestionItem.StringSpinnerQuestion -> HandlerStringSpinner(context, answers, languageID, ::goToNextQuestion, ::goToPreviousQuestion, ::showToast) is QuestionItem.StringSpinnerQuestion -> HandlerStringSpinner(context, answers, languageID, ::goToNextQuestion, ::goToPreviousQuestion, ::showToast, questionnaireMeta.id)
is QuestionItem.MultiCheckboxQuestion -> HandlerMultiCheckboxQuestion(context, answers, points, languageID, ::goToNextQuestion, ::goToPreviousQuestion, ::showToast) is QuestionItem.MultiCheckboxQuestion -> HandlerMultiCheckboxQuestion(context, answers, points, languageID, ::goToNextQuestion, ::goToPreviousQuestion, ::showToast, questionnaireMeta.id)
is QuestionItem.LastPage -> HandlerLastPage( is QuestionItem.LastPage -> HandlerLastPage(
answers, languageID, ::goToNextQuestion, ::goToPreviousQuestion answers, languageID, ::goToNextQuestion, ::goToPreviousQuestion
) { CoroutineScope(Dispatchers.IO).launch { saveAnswersToDatabase(answers, questionnaireMeta.id) } } ) { CoroutineScope(Dispatchers.IO).launch { saveAnswersToDatabase(answers, questionnaireMeta.id) } }
@ -177,11 +190,16 @@ abstract class QuestionnaireBase<T> {
val clientCode = answers["client_code"] as? String ?: return val clientCode = answers["client_code"] as? String ?: return
saveClientAndQuestionnaire(db, clientCode, questionnaireId) saveClientAndQuestionnaire(db, clientCode, questionnaireId)
// 🔥 Vor dem Speichern alte Antworten löschen
db.answerDao().deleteAnswersForClientAndQuestionnaire(clientCode, questionnaireId)
saveQuestions(db, answers, questionnaireId) saveQuestions(db, answers, questionnaireId)
saveAnswers(db, answers, questionnaireId, clientCode) saveAnswers(db, answers, questionnaireId, clientCode)
markQuestionnaireCompleted(db, questionnaireId, clientCode) markQuestionnaireCompleted(db, questionnaireId, clientCode)
} }
private suspend fun saveClientAndQuestionnaire(db: AppDatabase, clientCode: String, questionnaireId: String) { private suspend fun saveClientAndQuestionnaire(db: AppDatabase, clientCode: String, questionnaireId: String) {
db.clientDao().insertClient(Client(clientCode)) db.clientDao().insertClient(Client(clientCode))
db.questionnaireDao().insertQuestionnaire(Questionnaire(id = questionnaireId)) db.questionnaireDao().insertQuestionnaire(Questionnaire(id = questionnaireId))

View File

@ -1,6 +1,8 @@
package com.dano.test1 package com.dano.test1.questionnaire
import android.widget.Button import android.widget.Button
import com.dano.test1.LocalizationHelper
import com.dano.test1.R
open class QuestionnaireGeneric(private val questionnaireFileName: String) : QuestionnaireBase<Unit>() { open class QuestionnaireGeneric(private val questionnaireFileName: String) : QuestionnaireBase<Unit>() {

View File

@ -1,4 +1,4 @@
package com.dano.test1 package com.dano.test1.questionnaire
data class Option( data class Option(
val key: String, // Must always be set val key: String, // Must always be set
@ -112,11 +112,22 @@ sealed class QuestionItem {
val showPoints: Boolean = false // neu val showPoints: Boolean = false // neu
) )
data class Condition( // flexible Condition-Typen für die questionnaire_order.json
sealed class Condition {
object AlwaysAvailable : Condition()
data class RequiresCompleted(val required: List<String>) : Condition()
data class QuestionCondition(
val questionnaire: String, val questionnaire: String,
val questionId: String, val questionId: String,
val operator: String, val operator: String,
val value: String val value: String
) ) : Condition()
}
data class Combined(
val requiresCompleted: List<String>?,
val questionCheck: QuestionCondition?
) : Condition()
data class AnyOf(val conditions: List<Condition>) : Condition()
}
}

View File

@ -0,0 +1,145 @@
package com.dano.test1.questionnaire.handlers
import android.view.View
import android.widget.*
import com.dano.test1.questionnaire.GlobalValues
import com.dano.test1.LanguageManager
import com.dano.test1.MyApp
import com.dano.test1.questionnaire.QuestionHandler
import com.dano.test1.questionnaire.QuestionItem
import com.dano.test1.R
import com.dano.test1.network.TokenStore
import com.dano.test1.utils.ViewUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/*
Zweck :
- Steuert die Eingabeseite für „Client Code“ und „Coach Code“ innerhalb des Fragebogen-Flows.
*/
class HandlerClientCoachCode(
private val answers: MutableMap<String, Any>,
private val languageID: String,
private val goToNextQuestion: () -> Unit,
private val goToPreviousQuestion: () -> Unit,
private val showToast: (String) -> Unit,
) : QuestionHandler {
private lateinit var question: QuestionItem.ClientCoachCodeQuestion
private lateinit var layout: View
override fun bind(layout: View, question: QuestionItem) {
if (question !is QuestionItem.ClientCoachCodeQuestion) return
this.layout = layout
this.question = question
val clientCodeField = layout.findViewById<EditText>(R.id.client_code)
val coachCodeField = layout.findViewById<EditText>(R.id.coach_code)
val questionTextView = layout.findViewById<TextView>(R.id.question)
val titleTextView = layout.findViewById<TextView>(R.id.textView)
questionTextView.text = question.question?.let { LanguageManager.getText(languageID, it) } ?: ""
ViewUtils.setTextSizePercentOfScreenHeight(titleTextView, 0.03f)
ViewUtils.setTextSizePercentOfScreenHeight(questionTextView, 0.03f)
ViewUtils.setTextSizePercentOfScreenHeight(clientCodeField, 0.025f)
ViewUtils.setTextSizePercentOfScreenHeight(coachCodeField, 0.025f)
// Client-Code: nur verwenden, wenn bereits geladen
val loadedClientCode = GlobalValues.LOADED_CLIENT_CODE
if (!loadedClientCode.isNullOrBlank()) {
clientCodeField.setText(loadedClientCode)
clientCodeField.isEnabled = false
} else {
clientCodeField.setText("")
clientCodeField.isEnabled = true
}
// Coach-Code immer aus dem Login (TokenStore) setzen und sperren
val coachFromLogin = TokenStore.getUsername(layout.context)
if (!coachFromLogin.isNullOrBlank()) {
coachCodeField.setText(coachFromLogin)
ViewUtils.lockEditField(coachCodeField) // optisch & technisch gesperrt
} else {
// Falls (theoretisch) kein Login-Username vorhanden ist, verhalten wie bisher
coachCodeField.setText(answers["coach_code"] as? String ?: "")
coachCodeField.isEnabled = true
}
layout.findViewById<Button>(R.id.Qnext).setOnClickListener {
onNextClicked(clientCodeField, coachCodeField)
}
layout.findViewById<Button>(R.id.Qprev).setOnClickListener {
onPreviousClicked(clientCodeField, coachCodeField)
}
}
private fun onNextClicked(clientCodeField: EditText, coachCodeField: EditText) {
val loadedClientCode = GlobalValues.LOADED_CLIENT_CODE
if (!validate()) {
val message = LanguageManager.getText(languageID, "fill_both_fields")
showToast(message)
return
}
val clientCode = clientCodeField.text.toString()
// Erzwinge Coach-Code aus Login (falls vorhanden)
val coachCode = TokenStore.getUsername(layout.context) ?: coachCodeField.text.toString()
// Prüfen, ob die DB-Datei vor dem Zugriff existiert
val dbPath = layout.context.getDatabasePath("questionnaire_database")
val dbExistedBefore = dbPath.exists()
CoroutineScope(Dispatchers.IO).launch {
val existingClient = MyApp.database.clientDao().getClientByCode(clientCode)
withContext(Dispatchers.Main) {
if (existingClient != null && clientCodeField.isEnabled) {
val message = LanguageManager.getText(languageID, "client_code_exists")
showToast(message)
} else {
saveAnswers(clientCode, coachCode)
goToNextQuestion()
if (!dbExistedBefore) {
MyApp.database.close()
dbPath.delete()
val journalFile = layout.context.getDatabasePath("questionnaire_database-journal")
journalFile.delete()
}
}
}
}
}
private fun onPreviousClicked(clientCodeField: EditText, coachCodeField: EditText) {
val clientCode = clientCodeField.text.toString()
val coachCode = TokenStore.getUsername(layout.context) ?: coachCodeField.text.toString()
saveAnswers(clientCode, coachCode)
goToPreviousQuestion()
}
override fun validate(): Boolean {
val clientCode = layout.findViewById<EditText>(R.id.client_code).text
val coachText = layout.findViewById<EditText>(R.id.coach_code).text
return clientCode.isNotBlank() && coachText.isNotBlank()
}
private fun saveAnswers(clientCode: String, coachCode: String) {
GlobalValues.LAST_CLIENT_CODE = clientCode
answers["client_code"] = clientCode
// Speichere garantierten Coach-Code aus Login bevorzugt
val loginCoach = TokenStore.getUsername(layout.context)
answers["coach_code"] = loginCoach ?: coachCode
}
override fun saveAnswer() {
// Not used
}
}

View File

@ -1,7 +1,17 @@
package com.dano.test1 package com.dano.test1.questionnaire.handlers
import android.view.View import android.view.View
import android.widget.* import android.widget.*
import com.dano.test1.LanguageManager
import com.dano.test1.questionnaire.QuestionHandler
import com.dano.test1.questionnaire.QuestionItem
import com.dano.test1.R
/*
Zweck:
- Steuert die Seite Client hat nicht unterschrieben im Fragebogenfluss.
- Speichert den eingegebenen Coach-Code in das Answers-Map unter question.id (saveAnswer), damit der nachfolgende Prozess darauf zugreifen kann.
*/
class HandlerClientNotSigned( class HandlerClientNotSigned(
private val answers: MutableMap<String, Any>, private val answers: MutableMap<String, Any>,
@ -14,7 +24,6 @@ class HandlerClientNotSigned(
private lateinit var layout: View private lateinit var layout: View
private lateinit var question: QuestionItem.ClientNotSigned private lateinit var question: QuestionItem.ClientNotSigned
// UI components
private lateinit var textView1: TextView private lateinit var textView1: TextView
private lateinit var textView2: TextView private lateinit var textView2: TextView
private lateinit var questionTextView: TextView private lateinit var questionTextView: TextView
@ -26,29 +35,24 @@ class HandlerClientNotSigned(
this.layout = layout this.layout = layout
this.question = question this.question = question
// Initialize UI components only once
initViews() initViews()
// Set localized text values from LanguageManager
textView1.text = question.textKey1?.let { LanguageManager.getText(languageID, it) } ?: "" textView1.text = question.textKey1?.let { LanguageManager.getText(languageID, it) } ?: ""
textView2.text = question.textKey2?.let { LanguageManager.getText(languageID, it) } ?: "" textView2.text = question.textKey2?.let { LanguageManager.getText(languageID, it) } ?: ""
questionTextView.text = question.question?.let { LanguageManager.getText(languageID, it) } ?: "" questionTextView.text = question.question?.let { LanguageManager.getText(languageID, it) } ?: ""
// Populate EditText with previous value if exists
coachCodeField.setText(answers[question.id] as? String ?: "") coachCodeField.setText(answers[question.id] as? String ?: "")
// Set click listener for Next button
layout.findViewById<Button>(R.id.Qnext).setOnClickListener { layout.findViewById<Button>(R.id.Qnext).setOnClickListener {
onNextClicked() onNextClicked()
} }
// Set click listener for Previous button
layout.findViewById<Button>(R.id.Qprev).setOnClickListener { layout.findViewById<Button>(R.id.Qprev).setOnClickListener {
goToPreviousQuestion() goToPreviousQuestion()
} }
} }
// Initialize all views once to avoid repeated findViewById calls
private fun initViews() { private fun initViews() {
textView1 = layout.findViewById(R.id.textView1) textView1 = layout.findViewById(R.id.textView1)
textView2 = layout.findViewById(R.id.textView2) textView2 = layout.findViewById(R.id.textView2)
@ -56,7 +60,6 @@ class HandlerClientNotSigned(
coachCodeField = layout.findViewById(R.id.coach_code) coachCodeField = layout.findViewById(R.id.coach_code)
} }
// Handle Next button click
private fun onNextClicked() { private fun onNextClicked() {
if (validate()) { if (validate()) {
saveAnswer() saveAnswer()
@ -67,13 +70,11 @@ class HandlerClientNotSigned(
} }
} }
// Validate that coach code field is not empty
override fun validate(): Boolean { override fun validate(): Boolean {
val coachCode = coachCodeField.text val coachCode = coachCodeField.text
return coachCode.isNotBlank() return coachCode.isNotBlank()
} }
// Save entered coach code to answers map
override fun saveAnswer() { override fun saveAnswer() {
answers[question.id] = coachCodeField.text.toString() answers[question.id] = coachCodeField.text.toString()
} }

View File

@ -1,10 +1,26 @@
package com.dano.test1 package com.dano.test1.questionnaire.handlers
import android.content.Context import android.content.Context
import android.view.View import android.view.View
import android.widget.* import android.widget.*
import kotlinx.coroutines.*
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import com.dano.test1.questionnaire.GlobalValues
import com.dano.test1.LanguageManager
import com.dano.test1.questionnaire.MAX_VALUE_YEAR
import com.dano.test1.ui.Month
import com.dano.test1.ui.Months
import com.dano.test1.MyApp
import com.dano.test1.questionnaire.QuestionHandler
import com.dano.test1.questionnaire.QuestionItem
import com.dano.test1.R
import com.dano.test1.utils.ViewUtils
/*
Zweck:
Rendert eine Datumsfrage mit drei Spinnern (Tag/Monat/Jahr) innerhalb des Fragebogen-Flows.
*/
class HandlerDateSpinner( class HandlerDateSpinner(
private val context: Context, private val context: Context,
@ -12,7 +28,8 @@ class HandlerDateSpinner(
private val languageID: String, private val languageID: String,
private val goToNextQuestion: () -> Unit, private val goToNextQuestion: () -> Unit,
private val goToPreviousQuestion: () -> Unit, private val goToPreviousQuestion: () -> Unit,
private val showToast: (String) -> Unit private val showToast: (String) -> Unit,
private val questionnaireMeta: String // neu für DB-Abfrage
) : QuestionHandler { ) : QuestionHandler {
private lateinit var question: QuestionItem.DateSpinnerQuestion private lateinit var question: QuestionItem.DateSpinnerQuestion
@ -33,9 +50,21 @@ class HandlerDateSpinner(
val questionTextView = layout.findViewById<TextView>(R.id.question) val questionTextView = layout.findViewById<TextView>(R.id.question)
val textView = layout.findViewById<TextView>(R.id.textView) val textView = layout.findViewById<TextView>(R.id.textView)
val labelDay = layout.findViewById<TextView>(R.id.date_spinner_day)
val labelMonth = layout.findViewById<TextView>(R.id.date_spinner_month)
val labelYear = layout.findViewById<TextView>(R.id.date_spinner_year)
questionTextView.text = question.question?.let { LanguageManager.getText(languageID, it) } ?: "" questionTextView.text = question.question?.let { LanguageManager.getText(languageID, it) } ?: ""
textView.text = question.textKey?.let { LanguageManager.getText(languageID, it) } ?: "" textView.text = question.textKey?.let { LanguageManager.getText(languageID, it) } ?: ""
// Schriftgrößen pro Bildschirmhöhe
ViewUtils.setTextSizePercentOfScreenHeight(textView, 0.03f)
ViewUtils.setTextSizePercentOfScreenHeight(questionTextView, 0.03f)
ViewUtils.setTextSizePercentOfScreenHeight(labelDay, 0.025f)
ViewUtils.setTextSizePercentOfScreenHeight(labelMonth, 0.025f)
ViewUtils.setTextSizePercentOfScreenHeight(labelYear, 0.025f)
// gespeicherte Antwort (YYYY-MM-DD) lesen
val (savedYear, savedMonthIndex, savedDay) = question.question?.let { val (savedYear, savedMonthIndex, savedDay) = question.question?.let {
parseSavedDate(answers[it] as? String) parseSavedDate(answers[it] as? String)
} ?: Triple(null, null, null) } ?: Triple(null, null, null)
@ -50,9 +79,53 @@ class HandlerDateSpinner(
?: months[today.get(Calendar.MONTH)] ?: months[today.get(Calendar.MONTH)]
val defaultYear = savedYear ?: today.get(Calendar.YEAR) val defaultYear = savedYear ?: today.get(Calendar.YEAR)
setupSpinner(spinnerDay, days, defaultDay) // Spinner responsiv aufsetzen (Schrift + Zeilenhöhe ohne Abschneiden)
setupSpinner(spinnerMonth, months, defaultMonth) ViewUtils.setupResponsiveSpinner(context, spinnerDay, days, defaultDay)
setupSpinner(spinnerYear, years, defaultYear) ViewUtils.setupResponsiveSpinner(context, spinnerMonth, months, defaultMonth)
ViewUtils.setupResponsiveSpinner(context, spinnerYear, years, defaultYear)
// DB-Abfrage, falls noch nicht im answers-Map
val answerMapKey = question.question ?: (question.id ?: "")
if (answerMapKey.isNotBlank() && !answers.containsKey(answerMapKey)) {
CoroutineScope(Dispatchers.IO).launch {
try {
val clientCode = GlobalValues.LAST_CLIENT_CODE
if (clientCode.isNullOrBlank()) return@launch
val allAnswersForClient = MyApp.database.answerDao().getAnswersForClient(clientCode)
val myQuestionId = questionnaireMeta + "-" + question.question
val dbAnswer = allAnswersForClient.find { it.questionId == myQuestionId }?.answerValue
if (!dbAnswer.isNullOrBlank()) {
withContext(Dispatchers.Main) {
answers[answerMapKey] = dbAnswer
val (dbYear, dbMonthIndex, dbDay) = parseSavedDate(dbAnswer)
dbYear?.let { year ->
val index = years.indexOf(year)
if (index >= 0) spinnerYear.setSelection(index)
}
dbMonthIndex?.let { monthIndex ->
if (monthIndex in months.indices) {
val monthObj = months[monthIndex]
val idx = months.indexOf(monthObj)
if (idx >= 0) spinnerMonth.setSelection(idx)
}
}
dbDay?.let { day ->
val idx = days.indexOf(day)
if (idx >= 0) spinnerDay.setSelection(idx)
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
layout.findViewById<Button>(R.id.Qnext).setOnClickListener { layout.findViewById<Button>(R.id.Qnext).setOnClickListener {
if (validate()) { if (validate()) {
@ -139,16 +212,4 @@ class HandlerDateSpinner(
return sdf.parse(dateString) return sdf.parse(dateString)
} }
private fun <T> setupSpinner(spinner: Spinner, items: List<T>, defaultSelection: T?) {
val adapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, items)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinner.adapter = adapter
defaultSelection?.let {
val index = items.indexOf(it)
if (index >= 0) {
spinner.setSelection(index)
}
}
}
} }

View File

@ -0,0 +1,285 @@
package com.dano.test1.questionnaire.handlers
import android.content.Context
import android.view.Gravity
import android.view.View
import android.widget.*
import com.dano.test1.questionnaire.GlobalValues
import com.dano.test1.LanguageManager
import com.dano.test1.MyApp
import com.dano.test1.questionnaire.QuestionHandler
import com.dano.test1.questionnaire.QuestionItem
import com.dano.test1.R
import com.dano.test1.utils.ViewUtils
import kotlinx.coroutines.*
/*
Zweck:
- „Glas-Skala“-Frage: pro Symptom (Zeile) genau eine von fünf Stufen.
- Stufen als RadioButtons (clickbar) + Icon-Header.
- FIX: Pro Zeile wird Single-Select erzwungen (auch im Bearbeiten-Modus / Restore).
*/
class HandlerGlassScaleQuestion(
private val context: Context,
private val answers: MutableMap<String, Any>,
private val points: MutableList<Int>,
private val languageID: String,
private val goToNextQuestion: () -> Unit,
private val goToPreviousQuestion: () -> Unit,
private val showToast: (String) -> Unit,
private val questionnaireMeta: String
) : QuestionHandler {
private lateinit var layout: View
private lateinit var question: QuestionItem.GlassScaleQuestion
private val scaleLabels = listOf(
"never_glass",
"little_glass",
"moderate_glass",
"much_glass",
"extreme_glass"
)
private val pointsMap = mapOf(
"never_glass" to 0,
"little_glass" to 1,
"moderate_glass" to 2,
"much_glass" to 3,
"extreme_glass" to 4
)
private val glassIconForLabel = mapOf(
"never_glass" to R.drawable.ic_glass_0,
"little_glass" to R.drawable.ic_glass_1,
"moderate_glass" to R.drawable.ic_glass_2,
"much_glass" to R.drawable.ic_glass_3,
"extreme_glass" to R.drawable.ic_glass_4
)
// Damit wir beim programmatic-check keine Endlosschleifen triggern
private var suppressRowListener: Boolean = false
override fun bind(layout: View, question: QuestionItem) {
if (question !is QuestionItem.GlassScaleQuestion) return
this.layout = layout
this.question = question
val titleTv = layout.findViewById<TextView>(R.id.textView)
val questionTv = layout.findViewById<TextView>(R.id.question)
titleTv.text = question.textKey?.let { LanguageManager.getText(languageID, it) } ?: ""
questionTv.text = question.question?.let { LanguageManager.getText(languageID, it) } ?: ""
ViewUtils.setTextSizePercentOfScreenHeight(titleTv, 0.03f)
ViewUtils.setTextSizePercentOfScreenHeight(questionTv, 0.03f)
// Header Icons
val header = layout.findViewById<LinearLayout>(R.id.glass_header)
header.removeAllViews()
header.addView(Space(context).apply {
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 4f)
})
val iconSizePx = (context.resources.displayMetrics.density * 36).toInt()
scaleLabels.forEach { labelKey ->
val cell = FrameLayout(context).apply {
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
}
val img = ImageView(context).apply {
setImageResource(glassIconForLabel[labelKey]!!)
layoutParams = FrameLayout.LayoutParams(iconSizePx, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER)
adjustViewBounds = true
scaleType = ImageView.ScaleType.FIT_CENTER
}
cell.addView(img)
header.addView(cell)
}
val tableLayout = layout.findViewById<TableLayout>(R.id.glass_table)
tableLayout.removeAllViews()
addSymptomRows(tableLayout)
// Restore aus DB (nur wenn noch nicht im answers)
val anySymptomNeedsRestore = question.symptoms.any { !answers.containsKey(it) }
if (anySymptomNeedsRestore) {
CoroutineScope(Dispatchers.IO).launch {
try {
val clientCode = GlobalValues.LAST_CLIENT_CODE ?: return@launch
val allAnswersForClient = MyApp.database.answerDao().getAnswersForClient(clientCode)
val answerMap = allAnswersForClient.associateBy({ it.questionId }, { it.answerValue })
withContext(Dispatchers.Main) {
for ((index, symptomKey) in question.symptoms.withIndex()) {
val answerMapKey = "$questionnaireMeta-$symptomKey"
val dbAnswer = answerMap[answerMapKey]?.takeIf { it.isNotBlank() }?.trim()
if (!answers.containsKey(symptomKey) && !dbAnswer.isNullOrBlank()) {
if (index < tableLayout.childCount) {
val row = tableLayout.getChildAt(index) as? TableRow ?: continue
val radioGroup = row.getChildAt(1) as? RadioGroup ?: continue
setSingleSelection(radioGroup, dbAnswer) // <-- FIXED restore
answers[symptomKey] = dbAnswer
points.add(pointsMap[dbAnswer] ?: 0)
}
}
}
}
} catch (_: Exception) { /* ignore */ }
}
}
layout.findViewById<Button>(R.id.Qnext).setOnClickListener {
if (validate()) {
saveAnswer()
goToNextQuestion()
} else {
showToast(LanguageManager.getText(languageID, "select_one_answer_per_row"))
}
}
layout.findViewById<Button>(R.id.Qprev).setOnClickListener { goToPreviousQuestion() }
}
private fun addSymptomRows(table: TableLayout) {
question.symptoms.forEach { symptomKey ->
val savedLabel = answers[symptomKey] as? String
val row = TableRow(context).apply {
layoutParams = TableRow.LayoutParams(
TableRow.LayoutParams.MATCH_PARENT,
TableRow.LayoutParams.WRAP_CONTENT
)
}
val symptomText = TextView(context).apply {
text = LanguageManager.getText(languageID, symptomKey)
layoutParams = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT, 4f)
setPadding(4, 16, 4, 16)
ViewUtils.setTextSizePercentOfScreenHeight(this, 0.022f)
}
row.addView(symptomText)
val radioGroup = RadioGroup(context).apply {
orientation = RadioGroup.HORIZONTAL
layoutParams = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT, 5f)
setPadding(0, 0, 0, 0)
}
// Build buttons
scaleLabels.forEach { labelKey ->
val cell = FrameLayout(context).apply {
layoutParams = RadioGroup.LayoutParams(0, RadioGroup.LayoutParams.WRAP_CONTENT, 1f)
}
val rb = RadioButton(context).apply {
tag = labelKey
id = View.generateViewId()
isChecked = false
setPadding(0, 0, 0, 0)
}
rb.layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT,
Gravity.CENTER
)
// <<< FIX: erzwinge pro Zeile genau eine Auswahl
rb.setOnCheckedChangeListener { buttonView, isChecked ->
if (suppressRowListener) return@setOnCheckedChangeListener
if (!isChecked) return@setOnCheckedChangeListener
val selectedLabel = buttonView.tag as? String ?: return@setOnCheckedChangeListener
// wenn einer checked -> alle anderen in dieser Row unchecken
suppressRowListener = true
try {
for (i in 0 until radioGroup.childCount) {
val other = getRadioFromChild(radioGroup.getChildAt(i)) ?: continue
if (other != buttonView) other.isChecked = false
}
} finally {
suppressRowListener = false
}
// Optional (wenn du willst): sofort im answers setzen (Edit-Mode fühlt sich dann "direkt" an)
answers[symptomKey] = selectedLabel
}
cell.addView(rb)
radioGroup.addView(cell)
}
// Restore aus answers (falls vorhanden)
if (!savedLabel.isNullOrBlank()) {
setSingleSelection(radioGroup, savedLabel)
}
row.addView(radioGroup)
table.addView(row)
}
}
/**
* Setzt in diesem Row-RadioGroup genau einen Wert aktiv und alle anderen aus.
*/
private fun setSingleSelection(radioGroup: RadioGroup, labelKey: String) {
suppressRowListener = true
try {
for (i in 0 until radioGroup.childCount) {
val rb = getRadioFromChild(radioGroup.getChildAt(i)) ?: continue
val tag = (rb.tag as? String)?.trim()
rb.isChecked = (tag == labelKey.trim())
}
} finally {
suppressRowListener = false
}
}
override fun validate(): Boolean {
val table = layout.findViewById<TableLayout>(R.id.glass_table)
for (i in 0 until table.childCount) {
val row = table.getChildAt(i) as TableRow
val radioGroup = row.getChildAt(1) as RadioGroup
var anyChecked = false
for (j in 0 until radioGroup.childCount) {
val rb = getRadioFromChild(radioGroup.getChildAt(j)) ?: continue
if (rb.isChecked) { anyChecked = true; break }
}
if (!anyChecked) return false
}
return true
}
override fun saveAnswer() {
// alte Punkte entfernen
question.symptoms.forEach { key ->
val prev = answers[key] as? String
prev?.let { pointsMap[it] }?.let { points.remove(it) }
}
val table = layout.findViewById<TableLayout>(R.id.glass_table)
for (i in 0 until table.childCount) {
val row = table.getChildAt(i) as TableRow
val symptomKey = question.symptoms[i]
val radioGroup = row.getChildAt(1) as RadioGroup
for (j in 0 until radioGroup.childCount) {
val rb = getRadioFromChild(radioGroup.getChildAt(j)) ?: continue
if (rb.isChecked) {
val selected = rb.tag as String
answers[symptomKey] = selected
points.add(pointsMap[selected] ?: 0)
break
}
}
}
}
private fun getRadioFromChild(child: View): RadioButton? =
when (child) {
is RadioButton -> child
is FrameLayout -> child.getChildAt(0) as? RadioButton
else -> null
}
}

View File

@ -0,0 +1,151 @@
package com.dano.test1.questionnaire.handlers
import android.util.TypedValue
import android.view.View
import android.widget.*
import android.text.Html
import androidx.core.widget.TextViewCompat
import kotlinx.coroutines.*
import com.dano.test1.questionnaire.GlobalValues
import com.dano.test1.LanguageManager
import com.dano.test1.MainActivity
import com.dano.test1.questionnaire.QuestionHandler
import com.dano.test1.questionnaire.QuestionItem
import com.dano.test1.R
import com.dano.test1.utils.ViewUtils
import com.google.android.material.button.MaterialButton
/*
Zweck:
- Steuert die letzte Seite eines Fragebogens.
- Zeigt Abschlusstexte an, speichert alle gesammelten Antworten in die lokale DB und beendet anschließend den Fragebogen und kehrt zur übergeordneten Ansicht zurück.
Beim Klick auf „Speichern“:
- Ladezustand anzeigen (ProgressBar), Buttons deaktivieren.
- Antworten asynchron in Room-DB persistieren (über `saveAnswersToDatabase`).
- Punktsumme ermitteln und in `GlobalValues.INTEGRATION_INDEX` schreiben.
- `client_code` (falls vorhanden) als `GlobalValues.LAST_CLIENT_CODE` merken.
- Mindestens 2 Sekunden „Loading“-Dauer sicherstellen (ruhiges UX).
- Zurück auf den Main-Thread wechseln, UI entsperren und Fragebogen schließen.
*/
class HandlerLastPage(
private val answers: Map<String, Any>,
private val languageID: String,
private val goToNextQuestion: () -> Unit,
private val goToPreviousQuestion: () -> Unit,
private val saveAnswersToDatabase: suspend (Map<String, Any>) -> Unit
) : QuestionHandler {
private lateinit var currentQuestion: QuestionItem.LastPage
private lateinit var layout: View
private val minLoadingTimeMs = 2000L
override fun bind(layout: View, question: QuestionItem) {
this.layout = layout
currentQuestion = question as QuestionItem.LastPage
val titleTv = layout.findViewById<TextView>(R.id.textView)
val questionTv = layout.findViewById<TextView>(R.id.question)
val prevBtn = layout.findViewById<MaterialButton>(R.id.Qprev)
val finishBtn = layout.findViewById<MaterialButton>(R.id.Qfinish)
// Texte setzen
titleTv.text = LanguageManager.getText(languageID, currentQuestion.textKey)
questionTv.text = Html.fromHtml(
LanguageManager.getText(languageID, currentQuestion.question),
Html.FROM_HTML_MODE_LEGACY
)
// Finish-Button: Text + responsive Schrift
finishBtn.text = LanguageManager.getText(languageID, "save")
finishBtn.isAllCaps = false
applyResponsiveTextSizing(finishBtn)
// Überschriften responsiv skalieren (wie zuvor)
ViewUtils.setTextSizePercentOfScreenHeight(titleTv, 0.03f)
ViewUtils.setTextSizePercentOfScreenHeight(questionTv, 0.03f)
// Buttons
prevBtn.setOnClickListener { goToPreviousQuestion() }
finishBtn.setOnClickListener {
showLoading(true)
CoroutineScope(Dispatchers.IO).launch {
val startTime = System.currentTimeMillis()
// Antworten speichern
saveAnswersToDatabase(answers)
// Punkte summieren
GlobalValues.INTEGRATION_INDEX = sumPoints()
// Client-Code merken (für Auto-Laden im Opening Screen)
val clientCode = answers["client_code"] as? String
if (clientCode != null) {
GlobalValues.LAST_CLIENT_CODE = clientCode
GlobalValues.LOADED_CLIENT_CODE = clientCode // <— zusätzlich setzen
}
// min. Ladezeit einhalten (ruhiges UX)
val elapsedTime = System.currentTimeMillis() - startTime
if (elapsedTime < minLoadingTimeMs) delay(minLoadingTimeMs - elapsedTime)
withContext(Dispatchers.Main) {
showLoading(false)
val activity = layout.context as? MainActivity
// Zurück zum Opening Screen der lädt dann automatisch (siehe Änderung 2)
activity?.finishQuestionnaire() ?: goToNextQuestion()
}
}
}
}
override fun validate(): Boolean = true
override fun saveAnswer() {}
private fun applyResponsiveTextSizing(btn: MaterialButton) {
// Max-/Min-Sp anhand der Bildschirmhöhe (in sp) berechnen
val dm = btn.resources.displayMetrics
val maxSp = (dm.heightPixels * 0.028f) / dm.scaledDensity // ~2.8% der Höhe
val minSp = (dm.heightPixels * 0.018f) / dm.scaledDensity // ~1.8% der Höhe
// AutoSize aktivieren (schrumpft/expandiert den Text innerhalb des Buttons)
TextViewCompat.setAutoSizeTextTypeUniformWithConfiguration(
btn,
minSp.toInt(),
maxSp.toInt(),
1,
TypedValue.COMPLEX_UNIT_SP
)
btn.setSingleLine(true)
btn.maxLines = 1
btn.isAllCaps = false
// Padding nach Layout proportional zur Button-Höhe setzen (wirkt auf Lesbarkeit)
btn.post {
val padH = (btn.height * 0.18f).toInt()
val padV = (btn.height * 0.12f).toInt()
btn.setPadding(padH, padV, padH, padV)
}
}
// ----------------------------------------------------------------
private fun sumPoints(): Int =
answers.filterKeys { it.endsWith("_points") }
.values.mapNotNull { it as? Int }
.sum()
private fun showLoading(show: Boolean) {
val progressBar = layout.findViewById<ProgressBar>(R.id.progressBar)
val finishButton = layout.findViewById<Button>(R.id.Qfinish)
val prevButton = layout.findViewById<Button>(R.id.Qprev)
progressBar?.visibility = if (show) View.VISIBLE else View.GONE
finishButton?.isEnabled = !show
prevButton?.isEnabled = !show
}
}

View File

@ -0,0 +1,214 @@
package com.dano.test1.questionnaire.handlers
import android.content.Context
import android.util.TypedValue
import android.view.View
import android.widget.*
import androidx.core.widget.TextViewCompat
import kotlinx.coroutines.*
import com.dano.test1.questionnaire.GlobalValues
import com.dano.test1.LanguageManager
import com.dano.test1.MyApp
import com.dano.test1.questionnaire.QuestionHandler
import com.dano.test1.questionnaire.QuestionItem
import com.dano.test1.R
import com.dano.test1.utils.ViewUtils
/*
Zweck:
- Steuert eine Frage mit mehreren auswählbaren Antwortoptionen (Checkboxen).
*/
class HandlerMultiCheckboxQuestion(
private val context: Context,
private val answers: MutableMap<String, Any>,
private val points: MutableList<Int>,
private val languageID: String,
private val goToNextQuestion: () -> Unit,
private val goToPreviousQuestion: () -> Unit,
private val showToast: (String) -> Unit,
private val questionnaireMeta: String //
) : QuestionHandler {
private lateinit var layout: View
private lateinit var question: QuestionItem.MultiCheckboxQuestion
override fun bind(layout: View, question: QuestionItem) {
this.layout = layout
this.question = question as QuestionItem.MultiCheckboxQuestion
val container = layout.findViewById<LinearLayout>(R.id.CheckboxContainer)
val questionTitle = layout.findViewById<TextView>(R.id.question)
val questionTextView = layout.findViewById<TextView>(R.id.textView)
questionTextView.text = this.question.textKey?.let { LanguageManager.getText(languageID, it) } ?: ""
questionTitle.text = this.question.question?.let { LanguageManager.getText(languageID, it) } ?: ""
// Textgrößen pro Bildschirmhöhe (wie bei deinen anderen Handlern)
ViewUtils.setTextSizePercentOfScreenHeight(questionTextView, 0.03f)
ViewUtils.setTextSizePercentOfScreenHeight(questionTitle, 0.03f)
container.removeAllViews()
// bestehende Auswahl aus answers (falls vorhanden) als Set
val selectedKeys = this.question.question?.let {
(answers[it] as? List<*>)?.map { it.toString() }?.toSet()
} ?: emptySet()
// Checkbox-Schrift & Zeilenhöhe dynamisch ableiten (kein Abschneiden)
val dm = layout.resources.displayMetrics
val cbTextSp = (dm.heightPixels * 0.025f) / dm.scaledDensity // ~2.5% der Bildschirmhöhe
val cbTextPx = cbTextSp * dm.scaledDensity
val cbPadV = (cbTextPx * 0.40f).toInt()
val cbMinH = (cbTextPx * 1.60f + 2 * cbPadV).toInt()
this.question.options.forEach { option ->
val checkBox = CheckBox(context).apply {
text = LanguageManager.getText(languageID, option.key)
tag = option.key
isChecked = selectedKeys.contains(option.key)
// Textgröße prozentual & Zeilenhöhe/Padding für Lesbarkeit
TextViewCompat.setAutoSizeTextTypeWithDefaults(this, TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE)
setTextSize(TypedValue.COMPLEX_UNIT_SP, cbTextSp)
includeFontPadding = true
setPadding(paddingLeft, cbPadV, paddingRight, cbPadV)
minHeight = cbMinH
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
val scale = context.resources.displayMetrics.density
val marginBottom = (16 * scale + 0.5f).toInt()
setMargins(0, 0, 0, marginBottom)
}
}
container.addView(checkBox)
}
//DB-Abfrage falls noch kein Eintrag im answers-Map existiert
val answerMapKey = question.question ?: (question.id ?: "")
if (answerMapKey.isNotBlank() && !answers.containsKey(answerMapKey)) {
CoroutineScope(Dispatchers.IO).launch {
try {
val clientCode = GlobalValues.LAST_CLIENT_CODE
if (clientCode.isNullOrBlank()) return@launch
val allAnswersForClient = MyApp.database.answerDao().getAnswersForClient(clientCode)
val myQuestionId = questionnaireMeta + "-" + question.question
val dbAnswer = allAnswersForClient.find { it.questionId == myQuestionId }?.answerValue
if (!dbAnswer.isNullOrBlank()) {
val parsed = parseMultiAnswer(dbAnswer)
withContext(Dispatchers.Main) {
// UI: Checkboxen setzen
for (i in 0 until container.childCount) {
val cb = container.getChildAt(i) as? CheckBox ?: continue
cb.isChecked = parsed.contains(cb.tag.toString())
}
// answers-Map aktualisieren
answers[answerMapKey] = parsed.toList()
// Punkte berechnen und hinzufügen
val totalPoints = parsed.sumOf { key ->
question.pointsMap?.get(key) ?: 0
}
points.add(totalPoints)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
layout.findViewById<Button>(R.id.Qnext).setOnClickListener {
if (validate()) {
saveAnswer()
goToNextQuestion()
} else {
val msgKey = if (question.minSelection == 1) {
"select_at_least_one_answer"
} else {
"select_at_least_minimum"
}
val errorMessage = LanguageManager.getTextFormatted(languageID, msgKey, "choose_more_elements")
showToast(errorMessage)
}
}
layout.findViewById<Button>(R.id.Qprev).setOnClickListener {
goToPreviousQuestion()
}
}
override fun validate(): Boolean {
val container = layout.findViewById<LinearLayout>(R.id.CheckboxContainer)
var selectedCount = 0
for (i in 0 until container.childCount) {
val checkBox = container.getChildAt(i) as? CheckBox ?: continue
if (checkBox.isChecked) selectedCount++
}
return selectedCount >= question.minSelection
}
override fun saveAnswer() {
val container = layout.findViewById<LinearLayout>(R.id.CheckboxContainer)
val selectedKeys = mutableListOf<String>()
var totalPoints = 0
for (i in 0 until container.childCount) {
val checkBox = container.getChildAt(i) as? CheckBox ?: continue
if (checkBox.isChecked) {
val key = checkBox.tag.toString()
selectedKeys.add(key)
totalPoints += question.pointsMap?.get(key) ?: 0
}
}
question.question?.let { questionKey ->
// ggf. alte Punkte entfernen
val oldList = answers[questionKey] as? List<*>
if (oldList != null) {
val oldTotal = oldList.mapNotNull { it?.toString() }.sumOf { oldKey ->
question.pointsMap?.get(oldKey) ?: 0
}
points.remove(oldTotal)
}
answers[questionKey] = selectedKeys
points.add(totalPoints)
}
}
private fun parseMultiAnswer(dbAnswer: String): Set<String> {
val trimmed = dbAnswer.trim()
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
val inner = trimmed.substring(1, trimmed.length - 1)
if (inner.isBlank()) return emptySet()
return inner.split(",")
.map { it.trim().trim('"', '\'') }
.filter { it.isNotEmpty() }
.toSet()
}
val separator = when {
trimmed.contains(",") -> ","
trimmed.contains(";") -> ";"
else -> null
}
return if (separator != null) {
trimmed.split(separator)
.map { it.trim().trim('"', '\'') }
.filter { it.isNotEmpty() }
.toSet()
} else {
setOf(trimmed.trim().trim('"', '\''))
}
}
}

View File

@ -1,9 +1,22 @@
package com.dano.test1 package com.dano.test1.questionnaire.handlers
import android.content.Context import android.content.Context
import android.view.View import android.view.View
import android.text.Html import android.text.Html
import android.widget.* import android.widget.*
import kotlinx.coroutines.*
import com.dano.test1.questionnaire.GlobalValues
import com.dano.test1.LanguageManager
import com.dano.test1.MyApp
import com.dano.test1.questionnaire.QuestionHandler
import com.dano.test1.questionnaire.QuestionItem
import com.dano.test1.R
import com.dano.test1.utils.ViewUtils
/*
Zweck:
- Steuert eine Einzelfrage mit genau einer auswählbaren Antwort (RadioButtons).
*/
class HandlerRadioQuestion( class HandlerRadioQuestion(
private val context: Context, private val context: Context,
@ -13,7 +26,8 @@ class HandlerRadioQuestion(
private val goToNextQuestion: () -> Unit, private val goToNextQuestion: () -> Unit,
private val goToPreviousQuestion: () -> Unit, private val goToPreviousQuestion: () -> Unit,
private val goToQuestionById: (String) -> Unit, private val goToQuestionById: (String) -> Unit,
private val showToast: (String) -> Unit private val showToast: (String) -> Unit,
private val questionnaireMeta: String
) : QuestionHandler { ) : QuestionHandler {
private lateinit var layout: View private lateinit var layout: View
@ -32,6 +46,8 @@ class HandlerRadioQuestion(
Html.fromHtml(LanguageManager.getText(languageID, it), Html.FROM_HTML_MODE_LEGACY) Html.fromHtml(LanguageManager.getText(languageID, it), Html.FROM_HTML_MODE_LEGACY)
} ?: "" } ?: ""
ViewUtils.setTextSizePercentOfScreenHeight(questionTextView, 0.03f)
ViewUtils.setTextSizePercentOfScreenHeight(questionTitle, 0.03f)
radioGroup.removeAllViews() radioGroup.removeAllViews()
@ -39,6 +55,10 @@ class HandlerRadioQuestion(
val radioButton = RadioButton(context).apply { val radioButton = RadioButton(context).apply {
text = LanguageManager.getText(languageID, option.key) text = LanguageManager.getText(languageID, option.key)
tag = option.key tag = option.key
// RadioButton-Text analog zu EditTexts: 2.5% der Bildschirmhöhe
ViewUtils.setTextSizePercentOfScreenHeight(this, 0.025f)
layoutParams = RadioGroup.LayoutParams( layoutParams = RadioGroup.LayoutParams(
RadioGroup.LayoutParams.MATCH_PARENT, RadioGroup.LayoutParams.MATCH_PARENT,
RadioGroup.LayoutParams.WRAP_CONTENT RadioGroup.LayoutParams.WRAP_CONTENT
@ -47,12 +67,53 @@ class HandlerRadioQuestion(
val margin = (16 * scale + 0.5f).toInt() val margin = (16 * scale + 0.5f).toInt()
setMargins(0, 0, 0, margin) setMargins(0, 0, 0, margin)
} }
val padding = (12 * resources.displayMetrics.density).toInt()
setPadding(padding, padding, padding, padding)
} }
radioGroup.addView(radioButton) radioGroup.addView(radioButton)
} }
restorePreviousAnswer(radioGroup) restorePreviousAnswer(radioGroup)
val answerMapKey = question.question ?: (question.id ?: "")
if (answerMapKey.isNotBlank() && !answers.containsKey(answerMapKey)) {
CoroutineScope(Dispatchers.IO).launch {
try {
val clientCode = GlobalValues.LAST_CLIENT_CODE
if (clientCode.isNullOrBlank()) return@launch
val allAnswersForClient = MyApp.database.answerDao().getAnswersForClient(clientCode)
val myQuestionId = questionnaireMeta + "-" + question.question
val dbAnswer = allAnswersForClient.find { it.questionId == myQuestionId }?.answerValue
if (!dbAnswer.isNullOrBlank()) {
withContext(Dispatchers.Main) {
val oldAnswerKey = answers[answerMapKey] as? String
val oldPoint = oldAnswerKey?.let { question.pointsMap?.get(it) } ?: 0
if (oldAnswerKey != null) {
points.remove(oldPoint)
}
for (i in 0 until radioGroup.childCount) {
val radioButton = radioGroup.getChildAt(i) as RadioButton
if (radioButton.tag == dbAnswer) {
radioButton.isChecked = true
break
}
}
answers[answerMapKey] = dbAnswer
val newPoint = question.pointsMap?.get(dbAnswer) ?: 0
points.add(newPoint)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
layout.findViewById<Button>(R.id.Qnext).setOnClickListener { layout.findViewById<Button>(R.id.Qnext).setOnClickListener {
if (validate()) { if (validate()) {
saveAnswer() saveAnswer()
@ -104,10 +165,8 @@ class HandlerRadioQuestion(
val answerKey = selectedRadioButton.tag.toString() val answerKey = selectedRadioButton.tag.toString()
question.question?.let { questionKey -> question.question?.let { questionKey ->
val oldAnswerKey = answers[questionKey] as? String val oldAnswerKey = answers[questionKey] as? String
val oldPoint = oldAnswerKey?.let { question.pointsMap?.get(it) } ?: 0 val oldPoint = oldAnswerKey?.let { question.pointsMap?.get(it) } ?: 0
points.remove(oldPoint) points.remove(oldPoint)
answers[questionKey] = answerKey answers[questionKey] = answerKey
@ -116,6 +175,4 @@ class HandlerRadioQuestion(
points.add(newPoint) points.add(newPoint)
} }
} }
} }

View File

@ -1,8 +1,23 @@
package com.dano.test1 package com.dano.test1.questionnaire.handlers
import android.content.Context import android.content.Context
import android.view.View import android.view.View
import android.widget.* import android.widget.*
import kotlinx.coroutines.*
import com.dano.test1.ui.Countries
import com.dano.test1.questionnaire.GlobalValues
import com.dano.test1.LanguageManager
import com.dano.test1.MyApp
import com.dano.test1.questionnaire.QuestionHandler
import com.dano.test1.questionnaire.QuestionItem
import com.dano.test1.R
import com.dano.test1.utils.ViewUtils
/*
Zweck:
- Steuert eine Frage mit einer einzelnen Auswahl aus einer Dropdown-Liste (Spinner).
- Baut die Optionen dynamisch auf, lokalisiert Texte, stellt responsive Typografie her und kann vorhandene Antworten aus der lokalen Room-DB restaurieren.
*/
class HandlerStringSpinner( class HandlerStringSpinner(
private val context: Context, private val context: Context,
@ -10,7 +25,8 @@ class HandlerStringSpinner(
private val languageID: String, private val languageID: String,
private val goToNextQuestion: () -> Unit, private val goToNextQuestion: () -> Unit,
private val goToPreviousQuestion: () -> Unit, private val goToPreviousQuestion: () -> Unit,
private val showToast: (String) -> Unit private val showToast: (String) -> Unit,
private val questionnaireMeta: String
) : QuestionHandler { ) : QuestionHandler {
private lateinit var layout: View private lateinit var layout: View
@ -26,14 +42,46 @@ class HandlerStringSpinner(
val textView = layout.findViewById<TextView>(R.id.textView) val textView = layout.findViewById<TextView>(R.id.textView)
val spinner = layout.findViewById<Spinner>(R.id.string_spinner) val spinner = layout.findViewById<Spinner>(R.id.string_spinner)
// Texte setzen
questionTextView.text = question.question?.let { LanguageManager.getText(languageID, it) } ?: "" questionTextView.text = question.question?.let { LanguageManager.getText(languageID, it) } ?: ""
textView.text = question.textKey?.let { LanguageManager.getText(languageID, it) } ?: "" textView.text = question.textKey?.let { LanguageManager.getText(languageID, it) } ?: ""
// Textgrößen prozentual zur Bildschirmhöhe
ViewUtils.setTextSizePercentOfScreenHeight(textView, 0.03f)
ViewUtils.setTextSizePercentOfScreenHeight(questionTextView, 0.03f)
val options = buildOptionsList() val options = buildOptionsList()
// vorhandene Auswahl (falls vorhanden)
val savedSelection = question.question?.let { answers[it] as? String } val savedSelection = question.question?.let { answers[it] as? String }
setupSpinner(spinner, options, savedSelection) // Spinner aufsetzen
ViewUtils.setupResponsiveSpinner(context, spinner, options, savedSelection)
// Falls noch keine Antwort im Map: aus DB laden
val answerMapKey = question.question ?: (question.id ?: "")
if (answerMapKey.isNotBlank() && !answers.containsKey(answerMapKey)) {
CoroutineScope(Dispatchers.IO).launch {
try {
val clientCode = GlobalValues.LAST_CLIENT_CODE
if (clientCode.isNullOrBlank()) return@launch
val allAnswersForClient = MyApp.database.answerDao().getAnswersForClient(clientCode)
val myQuestionId = questionnaireMeta + "-" + question.question
val dbAnswer = allAnswersForClient.find { it.questionId == myQuestionId }?.answerValue
if (!dbAnswer.isNullOrBlank()) {
withContext(Dispatchers.Main) {
answers[answerMapKey] = dbAnswer
val index = options.indexOf(dbAnswer)
if (index >= 0) spinner.setSelection(index)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
layout.findViewById<Button>(R.id.Qnext).setOnClickListener { layout.findViewById<Button>(R.id.Qnext).setOnClickListener {
if (validate()) { if (validate()) {
@ -63,10 +111,7 @@ class HandlerStringSpinner(
override fun saveAnswer() { override fun saveAnswer() {
val spinner = layout.findViewById<Spinner>(R.id.string_spinner) val spinner = layout.findViewById<Spinner>(R.id.string_spinner)
val selected = spinner.selectedItem as? String ?: return val selected = spinner.selectedItem as? String ?: return
question.question?.let { key -> answers[key] = selected }
question.question?.let { key ->
answers[key] = selected
}
} }
private fun buildOptionsList(): List<String> { private fun buildOptionsList(): List<String> {
@ -78,16 +123,4 @@ class HandlerStringSpinner(
} }
} }
private fun <T> setupSpinner(spinner: Spinner, items: List<T>, selectedItem: T?) {
val adapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, items)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinner.adapter = adapter
selectedItem?.let {
val index = items.indexOf(it)
if (index >= 0) {
spinner.setSelection(index)
}
}
}
} }

View File

@ -1,8 +1,24 @@
package com.dano.test1 package com.dano.test1.questionnaire.handlers
import android.content.Context import android.content.Context
import android.view.View import android.view.View
import android.widget.* import android.widget.*
import kotlinx.coroutines.*
import com.dano.test1.questionnaire.GlobalValues
import com.dano.test1.LanguageManager
import com.dano.test1.MyApp
import com.dano.test1.questionnaire.QuestionHandler
import com.dano.test1.questionnaire.QuestionItem
import com.dano.test1.R
import com.dano.test1.utils.ViewUtils
/*
Zweck:
- Steuert eine Frage, bei der ein numerischer Wert aus einem Spinner gewählt wird.
- Unterstützt sowohl feste Optionslisten als auch numerische Bereiche (min..max).
- Lokalisiert Texte, kann eine frühere Antwort aus der lokalen Room-DB (per AnswerDao) wiederherstellen.
*/
class HandlerValueSpinner( class HandlerValueSpinner(
private val context: Context, private val context: Context,
@ -11,7 +27,8 @@ class HandlerValueSpinner(
private val goToNextQuestion: () -> Unit, private val goToNextQuestion: () -> Unit,
private val goToPreviousQuestion: () -> Unit, private val goToPreviousQuestion: () -> Unit,
private val goToQuestionById: (String) -> Unit, private val goToQuestionById: (String) -> Unit,
private val showToast: (String) -> Unit private val showToast: (String) -> Unit,
private val questionnaireMeta: String
) : QuestionHandler { ) : QuestionHandler {
private lateinit var layout: View private lateinit var layout: View
@ -30,6 +47,9 @@ class HandlerValueSpinner(
questionTextView.text = question.question?.let { LanguageManager.getText(languageID, it) } ?: "" questionTextView.text = question.question?.let { LanguageManager.getText(languageID, it) } ?: ""
textView.text = question.textKey?.let { LanguageManager.getText(languageID, it) } ?: "" textView.text = question.textKey?.let { LanguageManager.getText(languageID, it) } ?: ""
ViewUtils.setTextSizePercentOfScreenHeight(textView, 0.03f)
ViewUtils.setTextSizePercentOfScreenHeight(questionTextView, 0.03f)
val prompt = LanguageManager.getText(languageID, "choose_answer") val prompt = LanguageManager.getText(languageID, "choose_answer")
val spinnerItems: List<String> = listOf(prompt) + if (question.range != null) { val spinnerItems: List<String> = listOf(prompt) + if (question.range != null) {
(question.range.min..question.range.max).map { it.toString() } (question.range.min..question.range.max).map { it.toString() }
@ -38,7 +58,32 @@ class HandlerValueSpinner(
} }
val savedValue = question.question?.let { answers[it] as? String } val savedValue = question.question?.let { answers[it] as? String }
setupSpinner(spinner, spinnerItems, savedValue) ViewUtils.setupResponsiveSpinner(context, spinner, spinnerItems, savedValue)
//DB-Abfrage falls noch keine Antwort im Map existiert
val answerMapKey = question.question ?: (question.id ?: "")
if (answerMapKey.isNotBlank() && !answers.containsKey(answerMapKey)) {
CoroutineScope(Dispatchers.IO).launch {
try {
val clientCode = GlobalValues.LAST_CLIENT_CODE
if (clientCode.isNullOrBlank()) return@launch
val allAnswersForClient = MyApp.database.answerDao().getAnswersForClient(clientCode)
val myQuestionId = questionnaireMeta + "-" + question.question
val dbAnswer = allAnswersForClient.find { it.questionId == myQuestionId }?.answerValue
if (!dbAnswer.isNullOrBlank()) {
withContext(Dispatchers.Main) {
answers[answerMapKey] = dbAnswer
val index = spinnerItems.indexOf(dbAnswer)
if (index >= 0) spinner.setSelection(index)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
layout.findViewById<Button>(R.id.Qnext).setOnClickListener { layout.findViewById<Button>(R.id.Qnext).setOnClickListener {
if (validate()) { if (validate()) {
@ -84,14 +129,4 @@ class HandlerValueSpinner(
} }
} }
private fun <T> setupSpinner(spinner: Spinner, items: List<T>, selectedItem: T?) {
val adapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, items)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinner.adapter = adapter
selectedItem?.let {
val index = items.indexOf(it)
if (index >= 0) spinner.setSelection(index)
}
}
} }

View File

@ -1,4 +1,6 @@
package com.dano.test1 package com.dano.test1.ui
import com.dano.test1.LanguageManager
object Countries { object Countries {
fun getAllCountries(languageID: String): List<String> { fun getAllCountries(languageID: String): List<String> {

View File

@ -0,0 +1,470 @@
package com.dano.test1.ui
import android.graphics.Color
import android.graphics.Typeface
import android.util.Log
import android.view.View
import android.widget.*
import com.dano.test1.data.ExcelExportService
import com.dano.test1.utils.ViewUtils
import com.dano.test1.LanguageManager
import com.dano.test1.MainActivity
import com.dano.test1.MyApp
import com.dano.test1.R
import com.dano.test1.data.Client
import com.dano.test1.data.HeaderOrderRepository
import com.dano.test1.data.Question
import com.dano.test1.data.Questionnaire
import kotlinx.coroutines.*
import org.json.JSONArray
class DatabaseButtonHandler(
private val activity: MainActivity,
private val databaseButton: Button,
private val onClose: () -> Unit,
private val languageIDProvider: () -> String = { "GERMAN" }
) {
private val uiScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private val tag = "DatabaseButtonHandler"
private val headerRepo = HeaderOrderRepository(activity)
private val exporter = ExcelExportService(activity, headerRepo)
fun setup() {
val lang = safeLang()
databaseButton.text = t(lang, "database") ?: "Datenbank"
databaseButton.setOnClickListener { openDatabaseScreen() }
}
// ---------------------------
// SCREEN 1: Client-Liste
// ---------------------------
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")
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) }
progress.visibility = View.VISIBLE
emptyView.visibility = View.GONE
table.removeAllViews()
addHeaderRow(table, listOf("#", t(lang, "client_code") ?: "Client-Code"))
uiScope.launch {
val clients: List<Client> = withContext(Dispatchers.IO) {
MyApp.database.clientDao().getAllClients()
}
progress.visibility = View.GONE
if (clients.isEmpty()) {
emptyView.visibility = View.VISIBLE
return@launch
}
clients.forEachIndexed { index, client ->
addClickableRow(
table = table,
cells = listOf((index + 1).toString(), client.clientCode),
onClick = { openClientOverviewScreen(client.clientCode) }
)
}
}
}
// ---------------------------
// Export: Header aller Clients als Excel
// ---------------------------
private fun onDownloadHeadersClicked(progress: ProgressBar) {
val lang = safeLang()
uiScope.launch {
try {
progress.visibility = View.VISIBLE
val savedUri = exporter.exportHeadersForAllClients()
progress.visibility = View.GONE
if (savedUri != null) {
Toast.makeText(
activity,
t(lang, "export_success_downloads") ?: "Export erfolgreich: Downloads/ClientHeaders.xlsx",
Toast.LENGTH_LONG
).show()
} else {
Toast.makeText(activity, t(lang, "export_failed") ?: "Export fehlgeschlagen.", Toast.LENGTH_LONG).show()
}
} catch (e: Exception) {
progress.visibility = View.GONE
Log.e(tag, "Download Header Fehler: ${e.message}", e)
val prefix = t(lang, "error") ?: "Fehler"
Toast.makeText(activity, "$prefix: ${e.message}", Toast.LENGTH_LONG).show()
}
}
}
// ---------------------------
// 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")
val headerLabel: TextView = requireView(R.id.headerLabel, "headerLabel")
val tableOrdered: TableLayout = requireView(R.id.tableOrdered, "tableOrdered")
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
emptyView.visibility = View.GONE
tableQ.removeAllViews()
tableOrdered.removeAllViews()
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) {
val allQuestionnairesDb = MyApp.database.questionnaireDao().getAll()
val completedForClient = MyApp.database.completedQuestionnaireDao().getAllForClient(clientCode)
val allAnswersForClient = MyApp.database.answerDao().getAnswersForClient(clientCode)
Triple(allQuestionnairesDb, completedForClient, allAnswersForClient)
}
// IDs aus der JSON-Reihenfolge lesen (alle, die es geben soll)
val idsFromAssets: List<String> = loadQuestionnaireIdsFromAssets()
progress.visibility = View.GONE
val dbQuestionnaires: List<Questionnaire> = result.first
val completedForClient = result.second
val allAnswersForClient = result.third
// Vereinigung: alles was in JSON steht + alles was in der DB existiert
val allIds: List<String> =
(idsFromAssets + dbQuestionnaires.map { it.id }).distinct()
if (allIds.isEmpty()) {
emptyView.text = t(lang, "no_questionnaires") ?: "Keine Fragebögen vorhanden."
emptyView.visibility = View.VISIBLE
}
val statusMap = completedForClient.associate { it.questionnaireId to it.isDone }
val questionnaireIdSet = allIds.toSet() // für die zweite Tabelle
val answerMap = allAnswersForClient.associate { it.questionId to it.answerValue }
// Tabelle 1 (Status) JETZT mit allen IDs
allIds
.sortedWith(compareBy({ extractQuestionnaireNumber(it) ?: Int.MAX_VALUE }, { it }))
.forEachIndexed { idx, qid ->
val isDone = statusMap[qid] ?: 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(), qid, statusText),
onClick = { openQuestionnaireDetailScreen(clientCode, qid) },
colorOverrides = mapOf(2 to statusTextColor)
)
} else {
addDisabledRow(
table = tableQ,
cells = listOf((idx + 1).toString(), qid, statusText),
colorOverrides = mapOf(2 to statusTextColor)
)
}
}
// Farben
val lightGreen = 0xFFC8E6C9.toInt()
val lightRed = 0xFFFFCDD2.toInt()
val doneGreen = 0xFF4CAF50.toInt()
val notRed = 0xFFF44336.toInt()
// Tabelle 2 (Header-Liste)
val orderedIds = headerRepo.loadOrderedIds()
orderedIds.forEachIndexed { idx, id ->
var rowBgColor: Int? = null
val raw: String
val cellBgForQuestionnaire: Int?
if (id == "client_code") {
raw = clientCode
cellBgForQuestionnaire = null
} else if (id in questionnaireIdSet) {
raw = if (statusMap[id] == true) "Done" else "Not Done"
cellBgForQuestionnaire = if (raw == "Done") doneGreen else notRed
} else {
raw = answerMap[id]?.takeIf { it.isNotBlank() } ?: "None"
cellBgForQuestionnaire = null
rowBgColor = if (raw == "None") lightRed else lightGreen
}
val display = localizeHeaderValue(id, raw, lang)
val cellBgOverrides =
if (cellBgForQuestionnaire != null)
mapOf(0 to cellBgForQuestionnaire, 1 to cellBgForQuestionnaire, 2 to cellBgForQuestionnaire)
else emptyMap()
addRow(
table = tableOrdered,
cells = listOf((idx + 1).toString(), id, display),
rowBgColor = rowBgColor,
cellBgOverrides = cellBgOverrides
)
}
}
}
// ---------------------------
// 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 = "${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("#", t(lang, "question") ?: "Frage", t(lang, "answer") ?: "Antwort"))
uiScope.launch {
val (questions, answersForClient) = withContext(Dispatchers.IO) {
val qs = MyApp.database.questionDao().getQuestionsForQuestionnaire(questionnaireId)
val ans = MyApp.database.answerDao()
.getAnswersForClientAndQuestionnaire(clientCode, questionnaireId)
qs to ans
}
progress.visibility = View.GONE
if (questions.isEmpty()) {
emptyView.visibility = View.VISIBLE
return@launch
}
val answerMap = answersForClient.associate { it.questionId to it.answerValue }
questions.forEachIndexed { idx, q: Question ->
val baseId = q.questionId.substringAfterLast('-', q.questionId)
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))
}
}
}
// ---------------------------
// Hilfen
// ---------------------------
private fun safeLang(): String = try { languageIDProvider() } catch (_: Exception) { "GERMAN" }
private fun stripBrackets(s: String): String {
val m = Regex("^\\[(.*)]$").matchEntire(s.trim())
return m?.groupValues?.get(1) ?: s
}
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 localizeQuestionLabel(questionId: String, fallbackQuestionText: String?, lang: String): String {
val field = questionId.substringAfterLast('-', questionId)
t(lang, field)?.let { return it }
t(lang, questionId)?.let { return it }
fallbackQuestionText?.takeIf { it.isNotBlank() }?.let { return it }
return field.replace('_', ' ').replaceFirstChar { it.titlecase() }
}
private fun localizeAnswerValue(fieldId: String, raw: String, lang: String): String {
if (raw == "") return raw
if (raw.matches(Regex("^\\d{1,4}([./-]\\d{1,2}([./-]\\d{1,4})?)?\$"))) return raw
t(lang, raw)?.let { return it }
val norm = raw.lowercase().replace(Regex("[^a-z0-9]+"), "_").trim('_')
if (norm.isNotBlank()) t(lang, norm)?.let { return it }
if (norm.isNotBlank()) {
t(lang, "${fieldId}_$norm")?.let { return it }
t(lang, "${fieldId}-${norm}")?.let { return it }
}
return stripBrackets(raw)
}
private fun addHeaderRow(table: TableLayout, labels: List<String>) {
val row = TableRow(activity)
labels.forEach { label -> row.addView(makeHeaderCell(label)) }
table.addView(row); addDivider(table)
}
private fun addRow(
table: TableLayout,
cells: List<String>,
colorOverrides: Map<Int, Int> = emptyMap(),
rowBgColor: Int? = null,
cellBgOverrides: Map<Int, Int> = emptyMap()
) {
val row = TableRow(activity)
rowBgColor?.let { row.setBackgroundColor(it) }
cells.forEachIndexed { index, text ->
val tv = makeBodyCell(text, colorOverrides[index], cellBgOverrides[index]); row.addView(tv)
}
table.addView(row); addDivider(table)
}
private fun addClickableRow(
table: TableLayout,
cells: List<String>,
onClick: () -> Unit,
colorOverrides: Map<Int, Int> = emptyMap(),
cellBgOverrides: Map<Int, Int> = emptyMap()
) {
val row = TableRow(activity).apply {
isClickable = true
isFocusable = true
setBackgroundColor(Color.TRANSPARENT)
setOnClickListener { onClick() }
}
cells.forEachIndexed { index, text ->
val tv = makeBodyCell(text, colorOverrides[index], cellBgOverrides[index]); row.addView(tv)
}
table.addView(row); addDivider(table)
}
private fun addDisabledRow(
table: TableLayout,
cells: List<String>,
colorOverrides: Map<Int, Int> = emptyMap(),
cellBgOverrides: Map<Int, Int> = emptyMap()
) {
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)
}
table.addView(row); addDivider(table)
}
private fun addDivider(table: TableLayout) {
val divider = View(activity)
val params = TableLayout.LayoutParams(TableLayout.LayoutParams.MATCH_PARENT, dp(1))
divider.layoutParams = params
divider.setBackgroundColor(0xFFDDDDDD.toInt())
table.addView(divider)
}
private fun makeHeaderCell(text: String): TextView =
TextView(activity).apply {
this.text = text
setPadding(dp(12), dp(10), dp(12), dp(10))
textSize = 16f
setTypeface(typeface, Typeface.BOLD)
}
private fun makeBodyCell(
text: String,
textColor: Int? = null,
bgColor: Int? = null
): TextView =
TextView(activity).apply {
this.text = text
setPadding(dp(12), dp(10), dp(12), dp(10))
textSize = 15f
textColor?.let { setTextColor(it) }
bgColor?.let { setBackgroundColor(it) }
}
private fun dp(value: Int): Int = ViewUtils.dp(activity, value)
private fun <T : View> requireView(id: Int, name: String): T {
val v = activity.findViewById<T>(id)
if (v == null) {
val lang = safeLang()
val prefix = t(lang, "view_missing") ?: "Fehlende View: %s"
val msg = prefix.replace("%s", name)
Log.e(tag, msg)
Toast.makeText(activity, msg, Toast.LENGTH_LONG).show()
throw IllegalStateException(msg)
}
return v
}
private fun extractQuestionnaireNumber(id: String): Int? {
val m = Regex("^questionnaire_(\\d+)").find(id.lowercase())
return m?.groupValues?.get(1)?.toIntOrNull()
}
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)
}
/** Lädt alle Fragebogen-IDs aus questionnaire_order.json (file ohne .json). */
private fun loadQuestionnaireIdsFromAssets(): List<String> = try {
val input = activity.assets.open("questionnaire_order.json")
val json = input.bufferedReader().use { it.readText() }
val arr = JSONArray(json)
(0 until arr.length()).mapNotNull { i ->
val obj = arr.optJSONObject(i)
obj?.optString("file")?.removeSuffix(".json")
}
} catch (_: Exception) {
emptyList()
}
}

View File

@ -0,0 +1,97 @@
package com.dano.test1.ui
import android.widget.Button
import android.widget.EditText
import android.widget.Toast
import com.dano.test1.LanguageManager
import com.dano.test1.MainActivity
import com.dano.test1.MyApp
import kotlinx.coroutines.*
import com.dano.test1.data.CompletedQuestionnaire
import com.dano.test1.questionnaire.GlobalValues
class EditButtonHandler(
private val activity: MainActivity,
private val editButton: Button,
private val editText: EditText,
private val languageIDProvider: () -> String,
private val questionnaireFiles: Map<Button, String>,
private val buttonPoints: MutableMap<String, Int>,
private val updateButtonTexts: () -> Unit,
private val setButtonsEnabled: (List<Button>, Boolean) -> Unit,
private val setUiFreeze: (Boolean) -> Unit,
private val triggerLoad: () -> Unit
) {
fun setup() {
editButton.text = LanguageManager.getText(languageIDProvider(), "edit")
editButton.setOnClickListener { handleEditButtonClick() }
}
private fun handleEditButtonClick() {
val typed = editText.text.toString().trim()
val desiredCode = when {
typed.isNotBlank() -> typed
!GlobalValues.LOADED_CLIENT_CODE.isNullOrBlank() -> GlobalValues.LOADED_CLIENT_CODE!!
else -> ""
}
if (desiredCode.isBlank()) {
val message = LanguageManager.getText(languageIDProvider(), "please_client_code")
Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
return
}
GlobalValues.LAST_CLIENT_CODE = desiredCode
val needLoad = GlobalValues.LOADED_CLIENT_CODE?.equals(desiredCode) != true
if (needLoad) {
setUiFreeze(true) // Zwischenzustände unterdrücken
triggerLoad()
}
CoroutineScope(Dispatchers.IO).launch {
val loadedOk = waitUntilClientLoaded(desiredCode, timeoutMs = 2500, stepMs = 50)
if (!loadedOk) {
withContext(Dispatchers.Main) {
val msg = LanguageManager.getText(languageIDProvider(), "open_client_via_load")
Toast.makeText(activity, msg, Toast.LENGTH_LONG).show()
setUiFreeze(false)
}
return@launch
}
val completedEntries: List<CompletedQuestionnaire> =
MyApp.database.completedQuestionnaireDao().getAllForClient(desiredCode)
val completedFiles = completedEntries.filter { it.isDone }.map { it.questionnaireId.lowercase() }
buttonPoints.clear()
for (entry in completedEntries) {
if (entry.isDone) {
buttonPoints[entry.questionnaireId] = entry.sumPoints ?: 0
}
}
withContext(Dispatchers.Main) {
updateButtonTexts()
val enabledButtons = questionnaireFiles.filter { (_, fileName) ->
completedFiles.any { completedId -> fileName.lowercase().contains(completedId) }
}.keys.toList()
setButtonsEnabled(enabledButtons, true)
setUiFreeze(false)
}
}
}
private suspend fun waitUntilClientLoaded(expectedCode: String, timeoutMs: Long, stepMs: Long): Boolean {
if (GlobalValues.LOADED_CLIENT_CODE?.equals(expectedCode) == true) return true
var waited = 0L
while (waited < timeoutMs) {
delay(stepMs)
waited += stepMs
if (GlobalValues.LOADED_CLIENT_CODE?.equals(expectedCode) == true) return true
}
return GlobalValues.LOADED_CLIENT_CODE?.equals(expectedCode) == true
}
}

View File

@ -0,0 +1,724 @@
package com.dano.test1.ui
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.os.Handler
import android.os.Looper
import android.util.TypedValue
import android.view.Gravity
import android.view.View
import android.widget.*
import com.dano.test1.LanguageManager
import com.dano.test1.MainActivity
import com.dano.test1.R
import com.dano.test1.network.DatabaseUploader
import com.dano.test1.network.LoginManager
import com.dano.test1.network.NetworkUtils
import com.dano.test1.network.TokenStore
import com.dano.test1.questionnaire.GlobalValues
import com.dano.test1.questionnaire.QuestionItem
import com.dano.test1.questionnaire.QuestionnaireBase
import com.dano.test1.questionnaire.QuestionnaireGeneric
import com.google.android.material.button.MaterialButton
import org.json.JSONArray
import org.json.JSONObject
import java.io.File
import java.util.concurrent.TimeUnit
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.dano.test1.utils.ViewUtils
var RHS_POINTS: Int? = null
class HandlerOpeningScreen(private val activity: MainActivity) {
private var languageID: String = "GERMAN"
private lateinit var editText: EditText
private lateinit var coachEditText: EditText
private lateinit var spinner: Spinner
private lateinit var textView: TextView
private lateinit var buttonContainer: LinearLayout
private lateinit var buttonLoad: Button
private lateinit var saveButton: Button
private lateinit var editButton: Button
private lateinit var uploadButton: Button
private lateinit var downloadButton: Button
private lateinit var databaseButton: Button
private lateinit var statusSession: TextView
private lateinit var statusOnline: TextView
private val SESSION_WARN_AFTER_MS = 12 * 60 * 60 * 1000L // 12h
private var sessionLongWarnedOnce = false
private val dynamicButtons = mutableListOf<Button>()
private val questionnaireFiles = mutableMapOf<Button, String>()
private data class CardParts(
val title: TextView,
val subtitle: TextView,
val chip: TextView
)
private val cardParts = mutableMapOf<Button, CardParts>()
private val buttonPoints: MutableMap<String, Int> = mutableMapOf()
private var questionnaireEntries: List<QuestionItem.QuestionnaireEntry> = emptyList()
private var uiFreeze: Boolean = false
private val uiHandler = Handler(Looper.getMainLooper())
private val statusTicker = object : Runnable {
override fun run() {
updateStatusStrip()
uiHandler.postDelayed(this, 60_000)
}
}
private val STROKE_ENABLED = Color.parseColor("#8C79F2")
private val STROKE_DISABLED = Color.parseColor("#D8D3F5")
private fun t(id: String) = LanguageManager.getText(languageID, id)
fun init() {
activity.setContentView(R.layout.opening_screen)
// <<< NEU: bei jedem Öffnen des Screens zurücksetzen,
// damit der Toast pro Besuch einmal angezeigt wird
sessionLongWarnedOnce = false
bindViews()
loadQuestionnaireOrder()
createQuestionnaireButtons()
restorePreviousClientCode()
lockCoachCodeField()
setupLanguageSpinner()
setupLoadButton()
setupSaveButton()
setupEditButtonHandler()
setupUploadButton()
setupDownloadButton()
setupDatabaseButtonHandler()
uiHandler.removeCallbacks(statusTicker)
updateStatusStrip()
uiHandler.post(statusTicker)
val pathExists = File("/data/data/com.dano.test1/databases/questionnaire_database").exists()
updateMainButtonsState(pathExists)
updateDownloadButtonState(pathExists)
if (pathExists && !editText.text.isNullOrBlank()) buttonLoad.performClick()
uiHandler.removeCallbacks(statusTicker)
updateStatusStrip()
applySessionAgeHighlight(System.currentTimeMillis() - TokenStore.getLoginTimestamp(activity))
uiHandler.post(statusTicker)
}
private fun bindViews() {
editText = activity.findViewById(R.id.editText)
coachEditText = activity.findViewById(R.id.coachEditText)
spinner = activity.findViewById(R.id.string_spinner1)
textView = activity.findViewById(R.id.textView)
buttonContainer = activity.findViewById(R.id.buttonContainer)
buttonLoad = activity.findViewById(R.id.loadButton)
saveButton = activity.findViewById(R.id.saveButton)
editButton = activity.findViewById(R.id.editButton)
uploadButton = activity.findViewById(R.id.uploadButton)
downloadButton = activity.findViewById(R.id.downloadButton)
downloadButton.visibility = View.GONE
databaseButton = activity.findViewById(R.id.databaseButton)
statusSession = activity.findViewById(R.id.statusSession)
statusOnline = activity.findViewById(R.id.statusOnline)
val tag = editText.tag as? String ?: ""
editText.hint = t(tag)
val coachTag = coachEditText.tag as? String ?: ""
coachEditText.hint = t(coachTag)
textView.text = t("example_text")
}
private fun loadQuestionnaireOrder() {
try {
val inputStream = activity.assets.open("questionnaire_order.json")
val json = inputStream.bufferedReader().use { it.readText() }
val jsonArray = JSONArray(json)
questionnaireEntries = (0 until jsonArray.length()).map { i ->
val obj = jsonArray.getJSONObject(i)
val file = obj.getString("file")
val conditionObj = obj.optJSONObject("condition")
val condition = parseCondition(conditionObj)
val showPoints = obj.optBoolean("showPoints", false)
QuestionItem.QuestionnaireEntry(file, condition, showPoints)
}
} catch (_: Exception) {
questionnaireEntries = emptyList()
}
}
private fun parseCondition(conditionObj: JSONObject?): QuestionItem.Condition? {
if (conditionObj == null) return null
if (conditionObj.has("anyOf")) {
val arr = conditionObj.optJSONArray("anyOf") ?: JSONArray()
val conditions = mutableListOf<QuestionItem.Condition>()
for (i in 0 until arr.length()) {
val sub = arr.optJSONObject(i)
parseCondition(sub)?.let { conditions.add(it) }
}
return QuestionItem.Condition.AnyOf(conditions)
}
if (conditionObj.has("alwaysAvailable") && conditionObj.optBoolean("alwaysAvailable", false)) {
return QuestionItem.Condition.AlwaysAvailable
}
val requiresList = mutableListOf<String>()
if (conditionObj.has("requiresCompleted")) {
val reqArr = conditionObj.optJSONArray("requiresCompleted")
if (reqArr != null) {
for (i in 0 until reqArr.length()) requiresList.add(reqArr.optString(i))
} else {
conditionObj.optString("requiresCompleted")?.let { if (it.isNotBlank()) requiresList.add(it) }
}
}
val questionnaire = conditionObj.optString("questionnaire", null)
val questionId = conditionObj.optString("questionId", null)
val operator = conditionObj.optString("operator", null)
val value = conditionObj.optString("value", null)
val hasQuestionCheck =
!questionnaire.isNullOrBlank() && !questionId.isNullOrBlank() && !operator.isNullOrBlank() && value != null
return when {
requiresList.isNotEmpty() && hasQuestionCheck ->
QuestionItem.Condition.Combined(requiresList, QuestionItem.Condition.QuestionCondition(questionnaire!!, questionId!!, operator!!, value!!))
hasQuestionCheck ->
QuestionItem.Condition.QuestionCondition(questionnaire!!, questionId!!, operator!!, value!!)
requiresList.isNotEmpty() ->
QuestionItem.Condition.RequiresCompleted(requiresList)
else -> null
}
}
private fun createQuestionnaireButtons() {
buttonContainer.removeAllViews()
dynamicButtons.clear()
questionnaireFiles.clear()
cardParts.clear()
val vMargin = dp(8)
val startEnabled = mutableListOf<Button>()
questionnaireEntries.forEachIndexed { index, entry ->
val row = FrameLayout(activity).apply {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).also { it.setMargins(0, vMargin, 0, vMargin) }
}
val btn = MaterialButton(activity).apply {
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
dp(84)
)
text = ""
isAllCaps = false
setMinimumWidth(0)
setMinimumHeight(0)
cornerRadius = dp(22)
strokeWidth = dp(1)
strokeColor = ColorStateList.valueOf(STROKE_DISABLED)
backgroundTintList = ColorStateList.valueOf(Color.WHITE)
rippleColor = ColorStateList.valueOf(Color.parseColor("#22000000"))
gravity = Gravity.START or Gravity.CENTER_VERTICAL
setPadding(dp(20), 0, dp(20), 0)
id = View.generateViewId()
setOnClickListener {
GlobalValues.LAST_CLIENT_CODE = GlobalValues.LOADED_CLIENT_CODE
val fileName = questionnaireFiles[this] ?: return@setOnClickListener
val questionnaire = QuestionnaireGeneric(fileName)
startQuestionnaire(questionnaire)
applySetButtonsEnabled(dynamicButtons.filter { it == this }, allowCompleted = false, force = false)
}
}
val textColumn = LinearLayout(activity).apply {
orientation = LinearLayout.VERTICAL
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT,
Gravity.START or Gravity.CENTER_VERTICAL
).also { it.marginStart = dp(24) }
}
val tvTitle = TextView(activity).apply {
setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f)
setTextColor(Color.parseColor("#2F2A49"))
setPadding(0, dp(6), 0, dp(2))
}
val tvSubtitle = TextView(activity).apply {
setTextSize(TypedValue.COMPLEX_UNIT_SP, 13f)
setTextColor(Color.parseColor("#7B7794"))
visibility = View.GONE
}
textColumn.addView(tvTitle)
textColumn.addView(tvSubtitle)
val chip = TextView(activity).apply {
setPadding(dp(14), dp(8), dp(14), dp(8))
setTextColor(Color.WHITE)
text = t("start")
setBackgroundResource(R.drawable.bg_chip_amber)
setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f)
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT,
Gravity.END or Gravity.CENTER_VERTICAL
).also { it.marginEnd = dp(16) }
}
row.addView(btn)
row.addView(textColumn)
row.addView(chip)
buttonContainer.addView(row)
dynamicButtons.add(btn)
questionnaireFiles[btn] = entry.file
cardParts[btn] = CardParts(tvTitle, tvSubtitle, chip)
// Fallback-Titel lokalisieren
tvTitle.text = "${t("questionnaire")} ${index + 1}"
if (entry.condition is QuestionItem.Condition.AlwaysAvailable) startEnabled.add(btn)
}
applyUpdateButtonTexts(force = false)
applySetButtonsEnabled(startEnabled, allowCompleted = false, force = false)
}
private fun restorePreviousClientCode() {
// Coach-Code (Username) setzen und Feld sperren aber NICHT mehr zurückkehren
val username = TokenStore.getUsername(activity)
if (!username.isNullOrBlank()) {
coachEditText.setText(username)
lockCoachCodeField()
}
// Hier den zuletzt verwendeten Client-Code einsetzen
GlobalValues.LAST_CLIENT_CODE?.let {
editText.setText(it)
GlobalValues.LOADED_CLIENT_CODE = it
}
}
private fun setupLanguageSpinner() {
val languages = listOf("GERMAN", "ENGLISH", "FRENCH", "ROMANIAN", "ARABIC", "POLISH", "TURKISH", "UKRAINIAN", "RUSSIAN", "SPANISH")
val adapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, languages).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
}
spinner.adapter = adapter
spinner.setSelection(languages.indexOf(languageID))
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
languageID = languages[position]
applyUpdateButtonTexts(force = false)
val hintTag = editText.tag as? String ?: ""
editText.hint = t(hintTag)
val coachTag = coachEditText.tag as? String ?: ""
coachEditText.hint = t(coachTag)
}
override fun onNothingSelected(parent: AdapterView<*>) {}
}
}
private fun setupLoadButton() {
LoadButtonHandler(
activity = activity,
loadButton = buttonLoad,
editText = editText,
languageIDProvider = { languageID },
questionnaireEntriesProvider = { questionnaireEntries },
dynamicButtonsProvider = { dynamicButtons },
buttonPoints = buttonPoints,
updateButtonTexts = { applyUpdateButtonTexts(force = false) },
setButtonsEnabled = { list -> applySetButtonsEnabled(list, allowCompleted = false, force = false) },
updateMainButtonsState = { updateMainButtonsState(it) },
).setup()
}
private fun applyUpdateButtonTexts(force: Boolean) {
if (uiFreeze && !force) return
updateStatusStrip()
questionnaireFiles.forEach { (button, fileName) ->
val entry = questionnaireEntries.firstOrNull { it.file == fileName }
val key = fileName.substringAfter("questionnaire_").substringAfter("_").removeSuffix(".json")
val titleText = t(key)
val parts = cardParts[button] ?: return@forEach
parts.title.text = titleText
val points = buttonPoints.entries.firstOrNull { fileName.contains(it.key, ignoreCase = true) }?.value
val completed = isCompleted(button)
val enabled = button.isEnabled
val locked = !enabled && !completed
setClickableStroke(button, enabled)
if (locked) {
setLockedAppearance(button, true)
parts.title.setTextColor(Color.WHITE)
parts.subtitle.setTextColor(Color.parseColor("#E0E0E0"))
} else {
setLockedAppearance(button, false)
parts.title.setTextColor(Color.parseColor("#2F2A49"))
parts.subtitle.setTextColor(Color.parseColor("#7B7794"))
applyTintForButton(button, points, emphasize = enabled)
}
if (entry?.showPoints == true && points != null) {
parts.subtitle.visibility = View.VISIBLE
parts.subtitle.text = "${t("points")}: $points"
} else {
parts.subtitle.visibility = View.GONE
parts.subtitle.text = ""
}
when {
completed -> {
parts.chip.text = t("done")
parts.chip.setBackgroundResource(R.drawable.bg_chip_green)
parts.chip.setTextColor(Color.WHITE)
}
enabled -> {
parts.chip.text = t("start")
parts.chip.setBackgroundResource(R.drawable.bg_chip_amber)
parts.chip.setTextColor(Color.WHITE)
}
else -> {
parts.chip.text = t("locked")
parts.chip.setBackgroundResource(R.drawable.bg_chip_grey)
parts.chip.setTextColor(Color.parseColor("#2F2A49"))
}
}
}
buttonLoad.text = t("load")
saveButton.text = t("save")
editButton.text = t("edit")
uploadButton.text = t("upload")
downloadButton.text = t("download")
databaseButton.text = t("database")
val hintTag = editText.tag as? String ?: ""
editText.hint = t(hintTag)
val coachTag = coachEditText.tag as? String ?: ""
coachEditText.hint = t(coachTag)
textView.text = t("example_text")
}
private fun applySetButtonsEnabled(enabledButtons: List<Button>, allowCompleted: Boolean, force: Boolean) {
if (uiFreeze && !force) return
questionnaireFiles.keys.forEach { button ->
val completed = isCompleted(button)
val isAllowed = enabledButtons.contains(button)
val shouldEnable = if (allowCompleted) isAllowed else isAllowed && !completed
val locked = !shouldEnable && !completed
button.isEnabled = shouldEnable
button.alpha = if (completed || shouldEnable) 1.0f else 0.6f
setClickableStroke(button, shouldEnable)
cardParts[button]?.let { parts ->
if (locked) {
setLockedAppearance(button, true)
parts.title.setTextColor(Color.WHITE)
parts.subtitle.setTextColor(Color.parseColor("#E0E0E0"))
} else {
setLockedAppearance(button, false)
parts.title.setTextColor(Color.parseColor("#2F2A49"))
parts.subtitle.setTextColor(Color.parseColor("#7B7794"))
applyTintForButton(button, getPointsForButton(button), emphasize = shouldEnable)
}
when {
completed -> {
parts.chip.text = t("done")
parts.chip.setBackgroundResource(R.drawable.bg_chip_green)
parts.chip.setTextColor(Color.WHITE)
}
shouldEnable -> {
parts.chip.text = t("start")
parts.chip.setBackgroundResource(R.drawable.bg_chip_amber)
parts.chip.setTextColor(Color.WHITE)
}
else -> {
parts.chip.text = t("locked")
parts.chip.setBackgroundResource(R.drawable.bg_chip_grey)
parts.chip.setTextColor(Color.parseColor("#2F2A49"))
}
}
}
}
}
private fun startQuestionnaire(questionnaire: QuestionnaireBase<*>) {
activity.startQuestionnaire(questionnaire, languageID)
}
fun onBackPressed(): Boolean = false
private fun setupSaveButton() {
SaveButtonHandler(
activity = activity,
saveButton = saveButton,
editText = editText,
languageIDProvider = { languageID }
).setup()
}
private fun setupEditButtonHandler() {
EditButtonHandler(
activity = activity,
editButton = editButton,
editText = editText,
languageIDProvider = { languageID },
questionnaireFiles = questionnaireFiles,
buttonPoints = buttonPoints,
updateButtonTexts = { applyUpdateButtonTexts(force = true) },
setButtonsEnabled = { list, allowCompleted ->
applySetButtonsEnabled(list, allowCompleted, force = true)
},
setUiFreeze = { freeze -> uiFreeze = freeze },
triggerLoad = { buttonLoad.performClick() }
).setup()
}
private fun setupUploadButton() {
uploadButton.text = t("upload")
uploadButton.setOnClickListener {
// ZUERST bestätigen lassen
confirmUpload {
// === dein bestehender Upload-Code unverändert ===
val existingToken = TokenStore.getToken(activity)
val ageMs = System.currentTimeMillis() - TokenStore.getLoginTimestamp(activity)
val isFresh = !existingToken.isNullOrBlank() && ageMs < 23 * 60 * 60 * 1000
if (isFresh) {
GlobalValues.LAST_CLIENT_CODE = editText.text.toString().trim()
DatabaseUploader.uploadDatabaseWithToken(activity, existingToken!!)
return@confirmUpload
}
val username = TokenStore.getUsername(activity)?.trim().orEmpty()
if (username.isBlank()) {
Toast.makeText(activity, t("login_required"), Toast.LENGTH_LONG).show()
return@confirmUpload
}
val password = when (username) {
"user01" -> "pw1"
"user02" -> "pw2"
else -> {
Toast.makeText(activity, t("login_required"), Toast.LENGTH_LONG).show()
return@confirmUpload
}
}
LoginManager.loginUserWithCredentials(
context = activity,
username = username,
password = password,
onSuccess = { freshToken ->
GlobalValues.LAST_CLIENT_CODE = editText.text.toString().trim()
DatabaseUploader.uploadDatabaseWithToken(activity, freshToken)
},
onError = { msg ->
Toast.makeText(activity, t("login_failed_with_reason").replace("{reason}", msg), Toast.LENGTH_LONG).show()
}
)
}
}
}
private fun setupDownloadButton() {
downloadButton.text = t("download")
// Bei Setup gleich den aktuellen Zustand anwenden
val hasDb = File("/data/data/com.dano.test1/databases/questionnaire_database").exists()
updateDownloadButtonState(hasDb)
downloadButton.setOnClickListener {
// Falls der Button (später) deaktiviert ist, passiert einfach nichts
if (!downloadButton.isEnabled) return@setOnClickListener
Toast.makeText(activity, t("login_required"), Toast.LENGTH_SHORT).show()
}
}
private fun setupDatabaseButtonHandler() {
DatabaseButtonHandler(
activity = activity,
databaseButton = databaseButton,
onClose = { init() },
languageIDProvider = { languageID }
).setup()
}
private fun updateMainButtonsState(isDatabaseAvailable: Boolean) {
listOf(buttonLoad, saveButton, editButton, databaseButton).forEach { b ->
b.isEnabled = isDatabaseAvailable
b.alpha = if (isDatabaseAvailable) 1.0f else 0.5f
}
// Der Download-Button wird separat gesteuert
}
private fun updateDownloadButtonState(isDatabaseAvailable: Boolean) {
val mb = downloadButton as? MaterialButton
if (isDatabaseAvailable) {
downloadButton.isEnabled = false
downloadButton.alpha = 0.5f
mb?.apply {
strokeWidth = dp(1)
strokeColor = ColorStateList.valueOf(STROKE_DISABLED)
backgroundTintList = ColorStateList.valueOf(Color.parseColor("#F5F5F5"))
rippleColor = ColorStateList.valueOf(Color.parseColor("#00000000"))
}
} else {
downloadButton.isEnabled = true
downloadButton.alpha = 1.0f
mb?.apply {
strokeWidth = dp(2)
strokeColor = ColorStateList.valueOf(STROKE_ENABLED)
backgroundTintList = ColorStateList.valueOf(Color.WHITE)
rippleColor = ColorStateList.valueOf(Color.parseColor("#22000000"))
}
}
}
private fun dp(v: Int): Int = ViewUtils.dp(activity, v)
private fun isCompleted(button: Button): Boolean {
val fileName = questionnaireFiles[button] ?: return false
return buttonPoints.keys.any { k ->
fileName.contains(k, ignoreCase = true) || k.contains(fileName, ignoreCase = true)
}
}
private fun getPointsForButton(button: Button): Int? {
val fileName = questionnaireFiles[button] ?: return null
return buttonPoints.entries.firstOrNull { (k, _) ->
fileName.contains(k, ignoreCase = true) || k.contains(fileName, ignoreCase = true)
}?.value
}
private fun setLockedAppearance(button: Button, locked: Boolean) {
val mb = button as? MaterialButton ?: return
if (locked) {
mb.backgroundTintList = ColorStateList.valueOf(Color.BLACK)
mb.strokeColor = ColorStateList.valueOf(STROKE_DISABLED)
} else {
mb.backgroundTintList = ColorStateList.valueOf(Color.WHITE)
mb.strokeColor = ColorStateList.valueOf(if (button.isEnabled) STROKE_ENABLED else STROKE_DISABLED)
}
}
private fun resetTint(button: Button) {
val mb = button as? MaterialButton ?: return
mb.backgroundTintList = ColorStateList.valueOf(Color.WHITE)
mb.strokeColor = ColorStateList.valueOf(STROKE_DISABLED)
}
private fun applyTintForButton(button: Button, points: Int?, emphasize: Boolean) {
val file = questionnaireFiles[button] ?: return
val entry = questionnaireEntries.firstOrNull { it.file == file }
if (entry?.showPoints != true) {
val mb = button as? MaterialButton ?: return
mb.backgroundTintList = ColorStateList.valueOf(if (emphasize) Color.parseColor("#F1EEFF") else Color.WHITE)
mb.strokeColor = ColorStateList.valueOf(if (emphasize) STROKE_ENABLED else STROKE_DISABLED)
return
}
setScoreTint(button, points, emphasize)
}
private fun setClickableStroke(button: Button, enabled: Boolean) {
val mb = button as? MaterialButton ?: return
mb.strokeWidth = if (enabled) dp(2) else dp(1)
mb.strokeColor = ColorStateList.valueOf(if (enabled) STROKE_ENABLED else STROKE_DISABLED)
}
private fun setScoreTint(button: Button, points: Int?, emphasize: Boolean) {
val mb = button as? MaterialButton ?: return
val bg = when {
points == null && emphasize -> Color.parseColor("#F1EEFF")
points == null -> Color.parseColor("#FFFFFF")
points in 0..12 && emphasize -> Color.parseColor("#C8E6C9")
points in 0..12 -> Color.parseColor("#E8F5E9")
points in 13..36 && emphasize -> Color.parseColor("#FFE0B2")
points in 13..36 -> Color.parseColor("#FFF8E1")
points >= 37 && emphasize -> Color.parseColor("#FFCDD2")
else -> Color.parseColor("#FFEBEE")
}
mb.backgroundTintList = ColorStateList.valueOf(bg)
mb.strokeColor = ColorStateList.valueOf(if (emphasize) STROKE_ENABLED else STROKE_DISABLED)
}
private fun updateStatusStrip() {
val ts = TokenStore.getLoginTimestamp(activity)
val ageMs = if (ts > 0L) (System.currentTimeMillis() - ts) else 0L
val h = TimeUnit.MILLISECONDS.toHours(ageMs)
val m = TimeUnit.MILLISECONDS.toMinutes(ageMs) - h * 60
if (ts > 0L) {
// ⚠️ anhängen, wenn >12h, der eigentliche Hinweis/Styling kommt aus applySessionAgeHighlight()
val warn = if (ageMs >= SESSION_WARN_AFTER_MS) " ⚠️" else ""
statusSession.text = "${t("session_label")}: ${h}${t("hours_short")} ${m}${t("minutes_short")}$warn"
} else {
statusSession.text = t("session_dash")
}
val online = NetworkUtils.isOnline(activity)
statusOnline.text = if (online) t("online") else t("offline")
statusOnline.setTextColor(if (online) Color.parseColor("#2E7D32") else Color.parseColor("#C62828"))
// <<< NEU: hier jeweils prüfen/markieren
applySessionAgeHighlight(ageMs)
}
fun refreshHeaderStatusLive() {
applyUpdateButtonTexts(force = true)
}
private fun lockCoachCodeField() {
ViewUtils.lockEditField(coachEditText)
}
private fun applySessionAgeHighlight(ageMs: Long) {
val isOld = ageMs >= SESSION_WARN_AFTER_MS
if (isOld) {
statusSession.setTextColor(Color.parseColor("#C62828"))
statusSession.setBackgroundColor(Color.parseColor("#FFF3CD"))
statusSession.setPadding(dp(8), dp(4), dp(8), dp(4))
if (!sessionLongWarnedOnce) {
showRedToast(activity, t("session_over_12"))
sessionLongWarnedOnce = true
}
} else {
statusSession.setTextColor(Color.parseColor("#2F2A49"))
statusSession.setBackgroundColor(Color.TRANSPARENT)
statusSession.setPadding(0, 0, 0, 0)
}
}
private fun showRedToast(ctx: Context, message: String) {
val tv = TextView(ctx).apply {
text = message
setTextColor(Color.WHITE)
textSize = 16f
setPadding(32, 20, 32, 20)
background = GradientDrawable().apply {
shape = GradientDrawable.RECTANGLE
cornerRadius = 24f
setColor(Color.parseColor("#D32F2F")) // kräftiges Rot
}
}
Toast(ctx).apply {
duration = Toast.LENGTH_LONG
view = tv
setGravity(Gravity.TOP or Gravity.CENTER_HORIZONTAL, 0, 120)
}.show()
}
private fun confirmUpload(onConfirm: () -> Unit) {
MaterialAlertDialogBuilder(activity)
.setTitle(t("start_upload"))
.setMessage(t("ask_before_upload"))
.setPositiveButton(t("ok")) { d, _ ->
d.dismiss()
onConfirm()
}
.setNegativeButton(t("cancel")) { d, _ ->
d.dismiss()
}
.show()
}
}

View File

@ -0,0 +1,163 @@
package com.dano.test1.ui
import android.widget.Button
import android.widget.EditText
import android.widget.Toast
import com.dano.test1.LanguageManager
import com.dano.test1.MainActivity
import com.dano.test1.MyApp
import kotlinx.coroutines.*
import com.dano.test1.data.CompletedQuestionnaire
import com.dano.test1.questionnaire.GlobalValues
import com.dano.test1.questionnaire.QuestionItem
class LoadButtonHandler(
private val activity: MainActivity,
private val loadButton: Button,
private val editText: EditText,
private val languageIDProvider: () -> String,
private val questionnaireEntriesProvider: () -> List<QuestionItem.QuestionnaireEntry>,
private val dynamicButtonsProvider: () -> List<Button>,
private val buttonPoints: MutableMap<String, Int>,
private val updateButtonTexts: () -> Unit,
private val setButtonsEnabled: (List<Button>) -> Unit,
private val updateMainButtonsState: (Boolean) -> Unit,
) {
fun setup() {
loadButton.text = LanguageManager.getText(languageIDProvider(), "load")
loadButton.setOnClickListener { handleLoadButton() }
}
private fun handleLoadButton() {
val inputText = editText.text.toString().trim()
if (inputText.isBlank()) {
val message = LanguageManager.getText(languageIDProvider(), "please_client_code")
Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
return
}
buttonPoints.clear()
setButtonsEnabled(emptyList()) // temporär sperren
updateButtonTexts() // Chips zeigen vorläufig „Gesperrt“
val clientCode = inputText
GlobalValues.LAST_CLIENT_CODE = clientCode
CoroutineScope(Dispatchers.IO).launch {
val client = MyApp.database.clientDao().getClientByCode(clientCode)
if (client == null) {
GlobalValues.LOADED_CLIENT_CODE = null
withContext(Dispatchers.Main) {
val message = LanguageManager.getText(languageIDProvider(), "no_profile")
Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
val questionnaireEntries = questionnaireEntriesProvider()
val alwaysButtons = questionnaireEntries.mapIndexedNotNull { idx, entry ->
val btn = dynamicButtonsProvider().getOrNull(idx)
if (entry.condition is QuestionItem.Condition.AlwaysAvailable) btn else null
}
setButtonsEnabled(alwaysButtons)
updateButtonTexts() // <- nach dem Aktivieren Chips aktualisieren
}
return@launch
}
GlobalValues.LOADED_CLIENT_CODE = clientCode
withContext(Dispatchers.Main) { updateMainButtonsState(true) }
handleNormalLoad(clientCode)
}
}
private suspend fun evaluateCondition(
condition: QuestionItem.Condition?,
clientCode: String,
completedEntries: List<CompletedQuestionnaire>
): Boolean {
if (condition == null) return false
return when (condition) {
is QuestionItem.Condition.AlwaysAvailable -> true
is QuestionItem.Condition.RequiresCompleted -> {
val normalizedCompleted = completedEntries.map { normalizeQuestionnaireId(it.questionnaireId) }
condition.required.all { req ->
val nReq = normalizeQuestionnaireId(req)
normalizedCompleted.any { it.contains(nReq) || nReq.contains(it) }
}
}
is QuestionItem.Condition.QuestionCondition -> {
val answers = MyApp.database.answerDao().getAnswersForClientAndQuestionnaire(clientCode, condition.questionnaire)
val relevant = answers.find { it.questionId.endsWith(condition.questionId, ignoreCase = true) }
val answerValue = relevant?.answerValue ?: ""
when (condition.operator) {
"==" -> answerValue == condition.value
"!=" -> answerValue != condition.value
else -> false
}
}
is QuestionItem.Condition.Combined -> {
val normalizedCompleted = completedEntries.map { normalizeQuestionnaireId(it.questionnaireId) }
val reqOk = condition.requiresCompleted.isNullOrEmpty() || condition.requiresCompleted.all { req ->
val nReq = normalizeQuestionnaireId(req)
normalizedCompleted.any { it.contains(nReq) || nReq.contains(it) }
}
if (!reqOk) return false
val q = condition.questionCheck ?: return true
val answers = MyApp.database.answerDao().getAnswersForClientAndQuestionnaire(clientCode, q.questionnaire)
val relevant = answers.find { it.questionId.endsWith(q.questionId, ignoreCase = true) }
val answerValue = relevant?.answerValue ?: ""
when (q.operator) {
"==" -> answerValue == q.value
"!=" -> answerValue != q.value
else -> false
}
}
is QuestionItem.Condition.AnyOf -> {
condition.conditions.any { evaluateCondition(it, clientCode, completedEntries) }
}
}
}
private fun normalizeQuestionnaireId(name: String): String =
name.lowercase().removeSuffix(".json")
private suspend fun handleNormalLoad(clientCode: String) {
val completedEntries = withContext(Dispatchers.IO) {
MyApp.database.completedQuestionnaireDao().getAllForClient(clientCode)
}
buttonPoints.clear()
for (entry in completedEntries) {
if (entry.isDone) {
buttonPoints[entry.questionnaireId] = entry.sumPoints ?: 0
if (entry.questionnaireId.contains("questionnaire_2_rhs", ignoreCase = true)) {
RHS_POINTS = entry.sumPoints
}
}
}
val enabledButtons = mutableListOf<Button>()
val questionnaireEntries = questionnaireEntriesProvider()
val dynamicButtons = dynamicButtonsProvider()
for ((idx, entry) in questionnaireEntries.withIndex()) {
val button = dynamicButtons.getOrNull(idx) ?: continue
val isCompleted = completedEntries.any { completed ->
normalizeQuestionnaireId(completed.questionnaireId).let { completedNorm ->
val targetNorm = normalizeQuestionnaireId(entry.file)
(completedNorm.contains(targetNorm) || targetNorm.contains(completedNorm)) && completed.isDone
}
}
if (isCompleted) continue
val condMet = evaluateCondition(entry.condition, clientCode, completedEntries)
if (condMet) enabledButtons.add(button)
}
withContext(Dispatchers.Main) {
setButtonsEnabled(enabledButtons) // erst aktivieren …
updateButtonTexts() // … dann Chips/Labels korrekt setzen
}
}
}

View File

@ -1,4 +1,6 @@
package com.dano.test1 package com.dano.test1.ui
import com.dano.test1.LanguageManager
data class Month(val name: String) { data class Month(val name: String) {
override fun toString(): String = name override fun toString(): String = name

View File

@ -0,0 +1,230 @@
package com.dano.test1.ui
import android.content.ActivityNotFoundException
import android.content.ContentUris
import android.content.ContentValues
import android.content.Intent
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.pdf.PdfDocument
import android.provider.MediaStore
import android.util.Log
import android.widget.Button
import android.widget.EditText
import android.widget.Toast
import com.dano.test1.LanguageManager
import com.dano.test1.MainActivity
import com.dano.test1.MyApp
import com.dano.test1.questionnaire.GlobalValues
import kotlinx.coroutines.*
class SaveButtonHandler(
private val activity: MainActivity,
private val saveButton: Button,
private val editText: EditText,
private val languageIDProvider: () -> String
) {
fun setup() {
saveButton.text = LanguageManager.getText(languageIDProvider(), "save")
saveButton.setOnClickListener { handleSaveClick() }
}
private fun handleSaveClick() {
val clientCode = editText.text.toString().trim()
if (clientCode.isBlank()) {
val message = LanguageManager.getText(languageIDProvider(), "please_client_code")
Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
return
}
GlobalValues.LAST_CLIENT_CODE = clientCode
showCompletedQuestionnaires(clientCode)
}
private fun showCompletedQuestionnaires(clientCode: String) {
CoroutineScope(Dispatchers.IO).launch {
val actualClientCode = clientCode.removeSuffix("_database")
val completedEntries = MyApp.database.completedQuestionnaireDao().getAllForClient(actualClientCode)
Log.d("PDF_DEBUG", "Completed entries for client $actualClientCode:")
for (entry in completedEntries) {
Log.d("PDF_DEBUG", "Questionnaire ID: ${entry.questionnaireId}, Done: ${entry.isDone}, Points: ${entry.sumPoints}")
}
val pdfDocument = PdfDocument()
val pageWidth = 595
val pageHeight = 842
val paint = Paint().apply { textSize = 12f }
val csvBuilder = StringBuilder()
csvBuilder.appendLine("ClientCode,QuestionnaireID,IsDone,Points,Question,Answer")
for ((index, entry) in completedEntries.withIndex()) {
val pageInfo = PdfDocument.PageInfo.Builder(pageWidth, pageHeight, index + 1).create()
var page = pdfDocument.startPage(pageInfo)
var canvas = page.canvas
var yPosition = 40f
canvas.drawText("Client Code: $actualClientCode", 20f, yPosition, paint)
yPosition += 20f
canvas.drawText("Questionnaire: ${entry.questionnaireId}", 20f, yPosition, paint)
yPosition += 20f
canvas.drawText("Status: ${entry.isDone}", 20f, yPosition, paint)
yPosition += 20f
canvas.drawText("Points: ${entry.sumPoints ?: "N/A"}", 20f, yPosition, paint)
yPosition += 30f
val answers = MyApp.database.answerDao().getAnswersForClientAndQuestionnaire(actualClientCode, entry.questionnaireId)
for (answer in answers) {
val questionKey = answer.questionId.substringAfter("-")
val questionText = LanguageManager.getText("ENGLISH", questionKey)
val rawAnswerText = LanguageManager.getText("ENGLISH", answer.answerValue)
val answerText = rawAnswerText.trim().removePrefix("[").removeSuffix("]")
yPosition = drawMultilineText(canvas, "Question: $questionText", 20f, yPosition, paint, pageWidth - 40, isBold = true)
yPosition += 8f
yPosition = drawMultilineText(canvas, "Answer: $answerText", 20f, yPosition, paint, pageWidth - 40)
yPosition += 20f
paint.strokeWidth = 0.5f
canvas.drawLine(20f, yPosition - 30f, pageWidth - 20f, yPosition - 30f, paint)
paint.strokeWidth = 0f
val sanitizedQuestion = questionText.replace(",", " ").replace("\n", " ")
val sanitizedAnswer = answerText.replace(",", " ").replace("\n", " ")
csvBuilder.appendLine("${actualClientCode},${entry.questionnaireId},${entry.isDone},${entry.sumPoints ?: ""},\"$sanitizedQuestion\",\"$sanitizedAnswer\"")
if (yPosition > pageHeight - 60) {
pdfDocument.finishPage(page)
val newPageInfo = PdfDocument.PageInfo.Builder(pageWidth, pageHeight, pdfDocument.pages.size + 1).create()
page = pdfDocument.startPage(newPageInfo)
canvas = page.canvas
yPosition = 40f
}
}
pdfDocument.finishPage(page)
}
Log.d("CSV_OUTPUT", "Generated CSV:\n${csvBuilder}")
val pdfFileName = "DatabaseOutput_${actualClientCode}.pdf"
val csvFileName = "DatabaseOutput_${actualClientCode}.csv"
val resolver = activity.contentResolver
val deleteIfExists: (String) -> Unit = { name ->
val projection = arrayOf(MediaStore.MediaColumns._ID)
val selection = "${MediaStore.MediaColumns.DISPLAY_NAME} = ?"
val selectionArgs = arrayOf(name)
val query = resolver.query(
MediaStore.Downloads.EXTERNAL_CONTENT_URI,
projection, selection, selectionArgs, null
)
query?.use { cursor ->
if (cursor.moveToFirst()) {
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
val id = cursor.getLong(idColumn)
val deleteUri = ContentUris.withAppendedId(
MediaStore.Downloads.EXTERNAL_CONTENT_URI, id
)
resolver.delete(deleteUri, null, null)
}
}
}
deleteIfExists(pdfFileName)
deleteIfExists(csvFileName)
try {
val pdfUri = resolver.insert(
MediaStore.Downloads.EXTERNAL_CONTENT_URI,
ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, pdfFileName)
put(MediaStore.MediaColumns.MIME_TYPE, "application/pdf")
put(MediaStore.MediaColumns.RELATIVE_PATH, "Download/")
}
)
val csvUri = resolver.insert(
MediaStore.Downloads.EXTERNAL_CONTENT_URI,
ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, csvFileName)
put(MediaStore.MediaColumns.MIME_TYPE, "text/csv")
put(MediaStore.MediaColumns.RELATIVE_PATH, "Download/")
}
)
pdfUri?.let {
resolver.openOutputStream(it)?.use { out -> pdfDocument.writeTo(out) }
pdfDocument.close()
}
csvUri?.let {
resolver.openOutputStream(it)?.use { out ->
out.write(csvBuilder.toString().toByteArray(Charsets.UTF_8))
}
}
withContext(Dispatchers.Main) {
val msg = LanguageManager.getText(languageIDProvider(), "saved_pdf_csv")
Toast.makeText(activity, msg, Toast.LENGTH_LONG).show()
pdfUri?.let {
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(it, "application/pdf")
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NO_HISTORY)
}
try {
activity.startActivity(intent)
} catch (e: ActivityNotFoundException) {
val noViewer = LanguageManager.getText(languageIDProvider(), "no_pdf_viewer")
Toast.makeText(activity, noViewer, Toast.LENGTH_SHORT).show()
}
}
}
} catch (e: Exception) {
Log.e("SAVE", "Fehler beim Speichern der Dateien", e)
withContext(Dispatchers.Main) {
val errTpl = LanguageManager.getText(languageIDProvider(), "save_error")
val msg = (errTpl ?: "Fehler beim Speichern: {message}").replace("{message}", e.message ?: "")
Toast.makeText(activity, msg, Toast.LENGTH_LONG).show()
}
}
}
}
private fun drawMultilineText(
canvas: Canvas,
text: String,
x: Float,
yStart: Float,
paint: Paint,
maxWidth: Int,
isBold: Boolean = false
): Float {
paint.isFakeBoldText = isBold
val words = text.split(" ")
var line = ""
var y = yStart
for (word in words) {
val testLine = if (line.isEmpty()) word else "$line $word"
val lineWidth = paint.measureText(testLine)
if (lineWidth > maxWidth) {
canvas.drawText(line, x, y, paint)
y += paint.textSize * 1.4f
line = word
} else {
line = testLine
}
}
if (line.isNotEmpty()) {
canvas.drawText(line, x, y, paint)
y += paint.textSize * 1.4f
}
paint.isFakeBoldText = false
return y
}
}

View File

@ -0,0 +1,71 @@
package com.dano.test1
import java.io.File
import java.security.SecureRandom
import javax.crypto.Cipher
import javax.crypto.Mac
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import kotlin.math.min
object AES256Helper {
private fun hkdfFromToken(tokenHex: String, info: String = "qdb-aes", len: Int = 32): ByteArray {
val ikm = hexToBytes(tokenHex)
val mac = Mac.getInstance("HmacSHA256")
val zeroSalt = ByteArray(32) { 0 }
mac.init(SecretKeySpec(zeroSalt, "HmacSHA256"))
val prk = mac.doFinal(ikm)
var previous = ByteArray(0)
val okm = ByteArray(len)
var generated = 0
var counter = 1
while (generated < len) {
mac.init(SecretKeySpec(prk, "HmacSHA256"))
mac.update(previous)
mac.update(info.toByteArray(Charsets.UTF_8))
mac.update(counter.toByte())
val t = mac.doFinal()
val toCopy = min(len - generated, t.size)
System.arraycopy(t, 0, okm, generated, toCopy)
previous = t
generated += toCopy
counter++
}
return okm
}
private fun hexToBytes(hex: String): ByteArray {
val clean = hex.trim()
val len = clean.length
val out = ByteArray(len / 2)
var i = 0
while (i < len) {
out[i / 2] = ((Character.digit(clean[i], 16) shl 4) + Character.digit(clean[i + 1], 16)).toByte()
i += 2
}
return out
}
fun encryptFileWithToken(inFile: File, outFile: File, token: String) {
val key = hkdfFromToken(token)
val iv = ByteArray(16).also { SecureRandom().nextBytes(it) }
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
val plain = inFile.readBytes()
val enc = cipher.doFinal(plain)
outFile.writeBytes(iv + enc)
}
fun decryptFileWithToken(inFile: File, token: String): ByteArray {
val key = hkdfFromToken(token)
val data = inFile.readBytes()
require(data.size >= 16) { "cipher too short" }
val iv = data.copyOfRange(0, 16)
val ct = data.copyOfRange(16, data.size)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
return cipher.doFinal(ct)
}
}

View File

@ -0,0 +1,110 @@
package com.dano.test1.utils
import android.content.Context
import android.util.TypedValue
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.AbsListView
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.Spinner
import android.widget.TextView
import androidx.core.widget.TextViewCompat
import com.dano.test1.R
import kotlin.math.roundToInt
object ViewUtils {
/**
* Sets the text size of a TextView to a percentage of the screen height (in sp).
* Disables auto-sizing to prevent conflicts.
*/
fun setTextSizePercentOfScreenHeight(view: TextView, percentOfHeight: Float) {
val dm = view.context.resources.displayMetrics
val sp = (dm.heightPixels * percentOfHeight) / dm.scaledDensity
TextViewCompat.setAutoSizeTextTypeWithDefaults(view, TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE)
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, sp)
}
/**
* Sets up a Spinner with a responsive, styled adapter.
* Font size and row height are derived from screen height to prevent clipping.
*/
fun <T> setupResponsiveSpinner(context: Context, spinner: Spinner, items: List<T>, selectedItem: T?) {
val dm = context.resources.displayMetrics
fun spFromScreenHeight(percent: Float): Float = (dm.heightPixels * percent) / dm.scaledDensity
fun pxFromSp(sp: Float): Int = (sp * dm.scaledDensity).toInt()
val textSp = spFromScreenHeight(0.0275f)
val textPx = pxFromSp(textSp)
val vPadPx = (textPx * 0.50f).toInt()
val rowHeight = (textPx * 2.20f + 2 * vPadPx).toInt()
val adapter = object : ArrayAdapter<T>(context, android.R.layout.simple_spinner_item, items) {
private fun styleRow(tv: TextView, forceHeight: Boolean) {
tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSp)
tv.includeFontPadding = true
tv.setLineSpacing(0f, 1.2f)
tv.gravity = (tv.gravity and Gravity.HORIZONTAL_GRAVITY_MASK) or Gravity.CENTER_VERTICAL
tv.setPadding(tv.paddingLeft, vPadPx, tv.paddingRight, vPadPx)
tv.minHeight = rowHeight
tv.isSingleLine = true
if (forceHeight) {
val lp = tv.layoutParams
if (lp == null || lp.height <= 0) {
tv.layoutParams = AbsListView.LayoutParams(AbsListView.LayoutParams.MATCH_PARENT, rowHeight)
} else {
lp.height = rowHeight
}
}
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val v = super.getView(position, convertView, parent) as TextView
styleRow(v, forceHeight = false)
return v
}
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
val v = super.getDropDownView(position, convertView, parent) as TextView
styleRow(v, forceHeight = true)
return v
}
}
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinner.adapter = adapter
spinner.setPadding(spinner.paddingLeft, vPadPx, spinner.paddingRight, vPadPx)
spinner.minimumHeight = rowHeight
spinner.requestLayout()
selectedItem?.let {
val index = items.indexOf(it)
if (index >= 0) spinner.setSelection(index)
}
}
/**
* Locks an EditText field visually and functionally (e.g. for coach code fields).
*/
fun lockEditField(field: EditText, dpPadding: Int = 8) {
field.isFocusable = false
field.isFocusableInTouchMode = false
field.isCursorVisible = false
field.keyListener = null
field.isLongClickable = false
field.isClickable = false
field.setBackgroundResource(R.drawable.bg_field_locked)
field.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_lock_24, 0)
field.compoundDrawablePadding = dp(field.context, dpPadding)
field.alpha = 0.95f
}
/**
* Converts dp to pixels using the given context.
*/
fun dp(context: Context, value: Int): Int =
(value * context.resources.displayMetrics.density).roundToInt()
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="#FFFFFFFF"/>
</selector>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:alpha="0.2" android:color="#FFFFFF"/>
</selector>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:alpha="0.85" android:color="#334B5A" android:state_pressed="true"/>
<item android:color="#385266"/>
</selector>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="#FFFFFFFF"/>
</selector>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:alpha="0.2" android:color="#FFFFFF"/>
</selector>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:alpha="0.85" android:color="#2F855A" android:state_pressed="true"/>
<item android:color="#339966"/>
</selector>

View File

@ -0,0 +1,5 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="#F2B544"/>
<corners android:radius="18dp"/>
<padding android:left="8dp" android:top="6dp" android:right="8dp" android:bottom="6dp"/>
</shape>

View File

@ -0,0 +1,5 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="#43B581"/>
<corners android:radius="18dp"/>
<padding android:left="8dp" android:top="6dp" android:right="8dp" android:bottom="6dp"/>
</shape>

View File

@ -0,0 +1,5 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="#E3E5EF"/>
<corners android:radius="18dp"/>
<padding android:left="8dp" android:top="6dp" android:right="8dp" android:bottom="6dp"/>
</shape>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/brand_surface"/>
<stroke android:width="1dp" android:color="@color/brand_stroke"/>
<corners android:radius="12dp"/>
<padding android:left="8dp" android:top="6dp" android:right="8dp" android:bottom="6dp"/>
</shape>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="#F4F4F6"/>
<stroke android:width="1dp" android:color="#C7C7D0"/>
<corners android:radius="8dp"/>
<padding android:left="12dp" android:top="12dp" android:right="12dp" android:bottom="12dp"/>
</shape>

View File

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp" android:height="32dp"
android:viewportWidth="24" android:viewportHeight="24">
<path
android:fillColor="@android:color/transparent"
android:strokeColor="#FFFFFFFF"
android:strokeWidth="2.8"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M15,6 L9,12 L15,18" />
</vector>

View File

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp" android:height="32dp"
android:viewportWidth="24" android:viewportHeight="24">
<path
android:fillColor="@android:color/transparent"
android:strokeColor="#FFFFFFFF"
android:strokeWidth="2.8"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M9,6 L15,12 L9,18" />
</vector>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp" android:height="16dp"
android:viewportWidth="24" android:viewportHeight="24">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M12,6a6,6 0 1,0 0,12a6,6 0 1,0 0,-12z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp" android:height="48dp"
android:viewportWidth="64" android:viewportHeight="64">
<!-- Glas-Outline -->
<path android:fillColor="#00000000" android:strokeColor="#424242" android:strokeWidth="3"
android:pathData="M20,6h24a6,6 0 0 1 6,6v40a6,6 0 0 1 -6,6H20a6,6 0 0 1 -6,-6V12a6,6 0 0 1 6,-6z"/>
<!-- Füllung 0% -->
<path android:fillColor="#9FA8DA" android:pathData="M22,52H42V52H22z"/>
</vector>

View File

@ -0,0 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp" android:height="48dp"
android:viewportWidth="64" android:viewportHeight="64">
<path android:fillColor="#00000000" android:strokeColor="#424242" android:strokeWidth="3"
android:pathData="M20,6h24a6,6 0 0 1 6,6v40a6,6 0 0 1 -6,6H20a6,6 0 0 1 -6,-6V12a6,6 0 0 1 6,-6z"/>
<path android:fillColor="#9FA8DA" android:pathData="M22,46H42V52H22z"/>
</vector>

View File

@ -0,0 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp" android:height="48dp"
android:viewportWidth="64" android:viewportHeight="64">
<path android:fillColor="#00000000" android:strokeColor="#424242" android:strokeWidth="3"
android:pathData="M20,6h24a6,6 0 0 1 6,6v40a6,6 0 0 1 -6,6H20a6,6 0 0 1 -6,-6V12a6,6 0 0 1 6,-6z"/>
<path android:fillColor="#9FA8DA" android:pathData="M22,38H42V52H22z"/>
</vector>

View File

@ -0,0 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp" android:height="48dp"
android:viewportWidth="64" android:viewportHeight="64">
<path android:fillColor="#00000000" android:strokeColor="#424242" android:strokeWidth="3"
android:pathData="M20,6h24a6,6 0 0 1 6,6v40a6,6 0 0 1 -6,6H20a6,6 0 0 1 -6,-6V12a6,6 0 0 1 6,-6z"/>
<path android:fillColor="#9FA8DA" android:pathData="M22,30H42V52H22z"/>
</vector>

View File

@ -0,0 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp" android:height="48dp"
android:viewportWidth="64" android:viewportHeight="64">
<path android:fillColor="#00000000" android:strokeColor="#424242" android:strokeWidth="3"
android:pathData="M20,6h24a6,6 0 0 1 6,6v40a6,6 0 0 1 -6,6H20a6,6 0 0 1 -6,-6V12a6,6 0 0 1 6,-6z"/>
<path android:fillColor="#9FA8DA" android:pathData="M22,14H42V52H22z"/>
</vector>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:height="24dp"
android:viewportWidth="24" android:viewportHeight="24">
<path
android:fillColor="#8C79F2"
android:pathData="M12,17a2,2 0,1 0,0 -4 2,2 0,0 0,0 4zM18,8h-1V6a5,5 0,0 0,-10 0v2H6a2,2 0,0 0,-2 2v8a2,2 0,0 0,2 2h12a2,2 0,0 0,2 -2v-8a2,2 0,0 0,-2 -2zM8,6a4,4 0,0 1,8 0v2H8z"/>
</vector>

View File

@ -5,90 +5,121 @@
android:id="@+id/main" android:id="@+id/main"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".MainActivity" tools:context=".MainActivity">
tools:layout_editor_absoluteX="11dp"
tools:layout_editor_absoluteY="107dp">
<Button <androidx.constraintlayout.widget.Guideline
android:id="@+id/gTop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_begin="32dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/Qprev" android:id="@+id/Qprev"
android:tag="previous" android:layout_width="@dimen/nav_btn_size"
android:layout_width="130dp" android:layout_height="@dimen/nav_btn_size"
android:layout_height="42dp" android:layout_marginStart="20dp"
android:layout_marginBottom="16dp"
android:text=""
android:textAllCaps="false"
app:icon="@drawable/ic_chevron_left"
app:iconTint="@color/btn_nav_left_icon_tint"
app:iconSize="@dimen/nav_icon_size"
app:iconPadding="0dp"
app:cornerRadius="999dp"
app:backgroundTint="@color/btn_nav_left_tint"
app:rippleColor="@color/btn_nav_left_ripple"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" />
app:layout_constraintHorizontal_bias="0.056"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.976" />
<Button <com.google.android.material.button.MaterialButton
android:id="@+id/Qnext" android:id="@+id/Qnext"
android:tag="next" android:layout_width="@dimen/nav_btn_size"
android:layout_width="130dp" android:layout_height="@dimen/nav_btn_size"
android:layout_height="42dp" android:layout_marginEnd="20dp"
android:layout_marginBottom="16dp"
android:text=""
android:textAllCaps="false"
app:icon="@drawable/ic_chevron_right"
app:iconTint="@color/btn_nav_right_icon_tint"
app:iconSize="@dimen/nav_icon_size"
app:iconPadding="0dp"
app:cornerRadius="999dp"
app:backgroundTint="@color/btn_nav_right_tint"
app:rippleColor="@color/btn_nav_right_ripple"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent" />
app:layout_constraintHorizontal_bias="0.943"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.976" />
<EditText
android:id="@+id/client_code"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:background="@android:drawable/edit_text"
android:ems="10"
android:inputType="text"
android:tag="client_code"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.495"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.368" />
<EditText
android:id="@+id/coach_code"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:background="@android:drawable/edit_text"
android:ems="10"
android:inputType="text"
android:tag="coach_code"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.495"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.499" />
<TextView <TextView
android:id="@+id/textView" android:id="@+id/textView"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAlignment="center" android:gravity="center"
android:layout_marginStart="25dp" android:textStyle="bold"
android:layout_marginEnd="25dp" android:paddingStart="16dp"
app:layout_constraintBottom_toBottomOf="parent" android:paddingEnd="16dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/gTop"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintVertical_bias="0.104" /> app:layout_constraintWidth_percent="0.9" />
<TextView <TextView
android:id="@+id/question" android:id="@+id/question"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAlignment="center" android:gravity="center"
android:textStyle="bold" android:textStyle="bold"
android:layout_marginStart="25dp" android:paddingStart="16dp"
android:layout_marginEnd="25dp" android:paddingEnd="16dp"
app:layout_constraintBottom_toBottomOf="parent" android:paddingTop="8dp"
app:layout_constraintEnd_toEndOf="parent" android:paddingBottom="8dp"
app:layout_constraintTop_toBottomOf="@id/textView"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintVertical_bias="0.188" /> app:layout_constraintWidth_percent="0.9" />
<EditText
android:id="@+id/client_code"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintWidth_percent="0.7"
app:layout_constraintHeight_percent="0.08"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/question"
android:layout_marginTop="16dp"
android:background="@android:drawable/edit_text"
android:ems="10"
android:inputType="text"
android:tag="client_code"
android:gravity="center_vertical"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:autoSizeTextType="uniform"
android:autoSizeMinTextSize="12sp"
android:autoSizeMaxTextSize="36sp"
android:autoSizeStepGranularity="2sp" />
<EditText
android:id="@+id/coach_code"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintWidth_percent="0.7"
app:layout_constraintHeight_percent="0.08"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/client_code"
android:layout_marginTop="16dp"
android:background="@android:drawable/edit_text"
android:ems="10"
android:inputType="text"
android:tag="coach_code"
android:gravity="center_vertical"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:autoSizeTextType="uniform"
android:autoSizeMinTextSize="12sp"
android:autoSizeMaxTextSize="36sp"
android:autoSizeStepGranularity="2sp" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -9,29 +9,42 @@
tools:layout_editor_absoluteX="11dp" tools:layout_editor_absoluteX="11dp"
tools:layout_editor_absoluteY="107dp"> tools:layout_editor_absoluteY="107dp">
<Button <com.google.android.material.button.MaterialButton
android:id="@+id/Qprev" android:id="@+id/Qprev"
android:tag="previous" android:layout_width="@dimen/nav_btn_size"
android:layout_width="130dp" android:layout_height="@dimen/nav_btn_size"
android:layout_height="42dp" android:layout_marginStart="20dp"
android:layout_marginBottom="16dp"
android:text=""
android:textAllCaps="false"
app:icon="@drawable/ic_chevron_left"
app:iconTint="@color/btn_nav_left_icon_tint"
app:iconSize="@dimen/nav_icon_size"
app:iconPadding="0dp"
app:cornerRadius="999dp"
app:backgroundTint="@color/btn_nav_left_tint"
app:rippleColor="@color/btn_nav_left_ripple"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" />
app:layout_constraintHorizontal_bias="0.056"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.976" />
<Button <!-- Weiter -->
<com.google.android.material.button.MaterialButton
android:id="@+id/Qnext" android:id="@+id/Qnext"
android:tag="next" android:layout_width="@dimen/nav_btn_size"
android:layout_width="130dp" android:layout_height="@dimen/nav_btn_size"
android:layout_height="42dp" android:layout_marginEnd="20dp"
android:layout_marginBottom="16dp"
android:text=""
android:textAllCaps="false"
app:icon="@drawable/ic_chevron_right"
app:iconTint="@color/btn_nav_right_icon_tint"
app:iconSize="@dimen/nav_icon_size"
app:iconPadding="0dp"
app:cornerRadius="999dp"
app:backgroundTint="@color/btn_nav_right_tint"
app:rippleColor="@color/btn_nav_right_ripple"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent" />
app:layout_constraintHorizontal_bias="0.943"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.976" />
<EditText <EditText
@ -42,6 +55,7 @@
android:ems="10" android:ems="10"
android:inputType="text" android:inputType="text"
android:tag="coach_code" android:tag="coach_code"
android:textSize="30sp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.495" app:layout_constraintHorizontal_bias="0.495"
@ -53,6 +67,7 @@
android:id="@+id/textView2" android:id="@+id/textView2"
android:layout_width="329dp" android:layout_width="329dp"
android:layout_height="55dp" android:layout_height="55dp"
android:textSize="40sp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@ -63,6 +78,7 @@
android:id="@+id/question" android:id="@+id/question"
android:layout_width="329dp" android:layout_width="329dp"
android:layout_height="55dp" android:layout_height="55dp"
android:textSize="40sp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@ -73,6 +89,7 @@
android:id="@+id/textView1" android:id="@+id/textView1"
android:layout_width="329dp" android:layout_width="329dp"
android:layout_height="55dp" android:layout_height="55dp"
android:textSize="40sp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"

View File

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rootClientOverview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/titleClientOverview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Client Fragebögen"
android:textStyle="bold"
android:textSize="20sp"
android:paddingBottom="8dp" />
<!-- OBERTEIL: Alle Fragebögen vollständig (kein Weight, keine vertikale ScrollView) -->
<HorizontalScrollView
android:id="@+id/qsScroll"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fillViewport="true"
android:scrollbars="horizontal">
<TableLayout
android:id="@+id/tableQuestionnaires"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:stretchColumns="1" />
</HorizontalScrollView>
<TextView
android:id="@+id/emptyViewClient"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Keine Daten."
android:visibility="gone"
android:paddingTop="8dp" />
<ProgressBar
android:id="@+id/progressBarClient"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:paddingTop="8dp"
android:paddingBottom="8dp" />
<!-- HEADER + Tabelle 2: Scrollbar bekommt den restlichen Platz -->
<TextView
android:id="@+id/headerLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="header"
android:textStyle="bold"
android:textSize="18sp"
android:paddingTop="12dp"
android:paddingBottom="6dp" />
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:fillViewport="true"
android:scrollbars="horizontal">
<ScrollView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:fillViewport="true"
android:scrollbars="vertical">
<TableLayout
android:id="@+id/tableOrdered"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:stretchColumns="1,2" />
</ScrollView>
</HorizontalScrollView>
<Button
android:id="@+id/backButtonClient"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rootDatabase"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Datenbank Clients"
android:textStyle="bold"
android:textSize="20sp"
android:paddingBottom="8dp" />
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:fillViewport="true"
android:scrollbars="horizontal">
<ScrollView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:fillViewport="true"
android:scrollbars="vertical">
<TableLayout
android:id="@+id/tableClients"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:stretchColumns="1" />
</ScrollView>
</HorizontalScrollView>
<TextView
android:id="@+id/emptyView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Keine Clients vorhanden."
android:visibility="gone"
android:paddingTop="8dp" />
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
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"
android:layout_height="wrap_content"
android:text="Zurück" />
</LinearLayout>

View File

@ -1,128 +1,153 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/gTop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_begin="32dp" />
<TextView <TextView
android:id="@+id/textView" android:id="@+id/textView"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAlignment="center" android:gravity="center"
android:layout_marginStart="25dp" android:textStyle="bold"
android:layout_marginEnd="25dp" android:paddingStart="16dp"
app:layout_constraintBottom_toBottomOf="parent" android:paddingEnd="16dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/gTop"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintVertical_bias="0.104" /> app:layout_constraintWidth_percent="0.9" />
<TextView <TextView
android:id="@+id/question" android:id="@+id/question"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAlignment="center" android:gravity="center"
android:textStyle="bold" android:textStyle="bold"
android:layout_marginStart="25dp" android:paddingStart="16dp"
android:layout_marginEnd="25dp" android:paddingEnd="16dp"
app:layout_constraintBottom_toBottomOf="parent" android:paddingTop="8dp"
app:layout_constraintEnd_toEndOf="parent" android:paddingBottom="8dp"
app:layout_constraintTop_toBottomOf="@id/textView"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.188" />
<Spinner
android:id="@+id/spinner_value_month"
android:layout_width="142dp"
android:layout_height="35dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.453" app:layout_constraintWidth_percent="0.9" />
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.341" />
<TextView
android:id="@+id/date_spinner_year"
android:layout_width="110dp"
android:layout_height="22dp"
android:tag="year"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.877"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.282" />
<TextView <TextView
android:id="@+id/date_spinner_day" android:id="@+id/date_spinner_day"
android:layout_width="90dp" android:layout_width="0dp"
android:layout_height="22dp" android:layout_height="wrap_content"
android:tag="day" android:tag="day"
app:layout_constraintBottom_toBottomOf="parent" android:gravity="start|center_vertical"
android:textAlignment="viewStart"
android:paddingStart="8dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.099" app:layout_constraintHorizontal_bias="0.07"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toBottomOf="@id/question"
app:layout_constraintVertical_bias="0.282" /> app:layout_constraintWidth_percent="0.22" />
<Spinner
android:id="@+id/spinner_value_day"
android:layout_width="90dp"
android:layout_height="35dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.099"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.341" />
<TextView <TextView
android:id="@+id/date_spinner_month" android:id="@+id/date_spinner_month"
android:layout_width="142dp" android:layout_width="0dp"
android:layout_height="22dp" android:layout_height="wrap_content"
android:tag="month" android:tag="month"
app:layout_constraintBottom_toBottomOf="parent" android:gravity="start|center_vertical"
android:textAlignment="viewStart"
android:paddingStart="8dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.453" app:layout_constraintHorizontal_bias="0.459"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toBottomOf="@id/question"
app:layout_constraintVertical_bias="0.282" /> app:layout_constraintWidth_percent="0.40" />
<TextView
android:id="@+id/date_spinner_year"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="start|center_vertical"
android:paddingStart="8dp"
android:tag="year"
android:textAlignment="viewStart"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/question"
app:layout_constraintWidth_percent="0.32" />
<Spinner
android:id="@+id/spinner_value_day"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.07"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/date_spinner_day"
app:layout_constraintWidth_percent="0.22" />
<Spinner
android:id="@+id/spinner_value_month"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.459"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/date_spinner_month"
app:layout_constraintWidth_percent="0.40" />
<Spinner <Spinner
android:id="@+id/spinner_value_year" android:id="@+id/spinner_value_year"
android:layout_width="110dp" android:layout_width="0dp"
android:layout_height="35dp" android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent" android:layout_marginTop="8dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.877" app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toBottomOf="@id/date_spinner_year"
app:layout_constraintVertical_bias="0.341" /> app:layout_constraintWidth_percent="0.32" />
<Button <com.google.android.material.button.MaterialButton
android:id="@+id/Qnext"
android:layout_width="130dp"
android:layout_height="42dp"
android:tag="next"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.943"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.976" />
<Button
android:id="@+id/Qprev" android:id="@+id/Qprev"
android:layout_width="130dp" android:layout_width="@dimen/nav_btn_size"
android:layout_height="42dp" android:layout_height="@dimen/nav_btn_size"
android:tag="previous" android:layout_marginStart="20dp"
android:layout_marginBottom="16dp"
android:text=""
android:textAllCaps="false"
app:icon="@drawable/ic_chevron_left"
app:iconTint="@color/btn_nav_left_icon_tint"
app:iconSize="@dimen/nav_icon_size"
app:iconPadding="0dp"
app:cornerRadius="999dp"
app:backgroundTint="@color/btn_nav_left_tint"
app:rippleColor="@color/btn_nav_left_ripple"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" />
app:layout_constraintHorizontal_bias="0.056"
app:layout_constraintStart_toStartOf="parent" <com.google.android.material.button.MaterialButton
app:layout_constraintTop_toTopOf="parent" android:id="@+id/Qnext"
app:layout_constraintVertical_bias="0.976" /> android:layout_width="@dimen/nav_btn_size"
android:layout_height="@dimen/nav_btn_size"
android:layout_marginEnd="20dp"
android:layout_marginBottom="16dp"
android:text=""
android:textAllCaps="false"
app:icon="@drawable/ic_chevron_right"
app:iconTint="@color/btn_nav_right_icon_tint"
app:iconSize="@dimen/nav_icon_size"
app:iconPadding="0dp"
app:cornerRadius="999dp"
app:backgroundTint="@color/btn_nav_right_tint"
app:rippleColor="@color/btn_nav_right_ripple"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,17 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/gTop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_begin="32dp" />
<TextView
android:id="@+id/textView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center"
android:textStyle="bold"
android:paddingStart="16dp"
android:paddingEnd="16dp"
app:layout_constraintTop_toBottomOf="@id/gTop"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintWidth_percent="0.9" />
<TextView
android:id="@+id/question"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center"
android:textStyle="bold"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
app:layout_constraintTop_toBottomOf="@id/textView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintWidth_percent="0.9" />
<LinearLayout
android:id="@+id/glass_header"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp"
android:weightSum="9"
app:layout_constraintTop_toBottomOf="@id/question"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintWidth_percent="1" />
<ScrollView <ScrollView
android:layout_width="match_parent" android:id="@+id/glassScroll"
android:layout_height="466dp" android:layout_width="0dp"
android:layout_height="0dp"
android:fillViewport="true" android:fillViewport="true"
app:layout_constraintTop_toBottomOf="@+id/question" android:clipToPadding="false"
android:paddingTop="4dp"
android:paddingBottom="8dp"
app:layout_constraintTop_toBottomOf="@id/glass_header"
app:layout_constraintBottom_toTopOf="@+id/Qprev" app:layout_constraintBottom_toTopOf="@+id/Qprev"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"> app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintWidth_percent="1">
<TableLayout <TableLayout
android:id="@+id/glass_table" android:id="@+id/glass_table"
@ -23,49 +75,36 @@
android:textStyle="bold" /> android:textStyle="bold" />
</ScrollView> </ScrollView>
<com.google.android.material.button.MaterialButton
<Button
android:id="@+id/Qprev" android:id="@+id/Qprev"
android:layout_width="130dp" android:layout_width="@dimen/nav_btn_size"
android:layout_height="wrap_content" android:layout_height="@dimen/nav_btn_size"
android:tag="previous" android:layout_marginStart="20dp"
android:layout_marginBottom="16dp"
app:icon="@drawable/ic_chevron_left"
app:iconTint="@color/btn_nav_left_icon_tint"
app:iconSize="@dimen/nav_icon_size"
app:iconPadding="0dp"
app:cornerRadius="999dp"
app:backgroundTint="@color/btn_nav_left_tint"
app:rippleColor="@color/btn_nav_left_ripple"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent" />
android:layout_margin="16dp" />
<Button <com.google.android.material.button.MaterialButton
android:id="@+id/Qnext" android:id="@+id/Qnext"
android:layout_width="130dp" android:layout_width="@dimen/nav_btn_size"
android:layout_height="wrap_content" android:layout_height="@dimen/nav_btn_size"
android:tag="next" android:layout_marginEnd="20dp"
android:layout_marginBottom="16dp"
app:icon="@drawable/ic_chevron_right"
app:iconTint="@color/btn_nav_right_icon_tint"
app:iconSize="@dimen/nav_icon_size"
app:iconPadding="0dp"
app:cornerRadius="999dp"
app:backgroundTint="@color/btn_nav_right_tint"
app:rippleColor="@color/btn_nav_right_ripple"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent" />
android:layout_margin="16dp" />
<TextView
android:id="@+id/textView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textAlignment="center"
android:layout_marginStart="25dp"
android:layout_marginEnd="25dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.104" />
<TextView
android:id="@+id/question"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textAlignment="center"
android:textStyle="bold"
android:layout_marginStart="25dp"
android:layout_marginEnd="25dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.188" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -5,57 +5,77 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<TextView <!-- Zurück (links unten) -->
android:id="@+id/question" <com.google.android.material.button.MaterialButton
android:layout_width="389dp"
android:layout_height="115dp"
android:tag="finish_data_entry"
android:textAlignment="viewStart"
android:textSize="24sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.376" />
<Button
android:id="@+id/Qprev" android:id="@+id/Qprev"
android:layout_width="130dp" android:layout_width="@dimen/nav_btn_size"
android:layout_height="42dp" android:layout_height="@dimen/nav_btn_size"
android:tag="previous" android:layout_marginStart="20dp"
android:layout_marginBottom="16dp"
android:text=""
android:textAllCaps="false"
app:icon="@drawable/ic_chevron_left"
app:iconTint="@color/btn_nav_left_icon_tint"
app:iconSize="@dimen/nav_icon_size"
app:iconPadding="0dp"
app:cornerRadius="999dp"
app:backgroundTint="@color/btn_nav_left_tint"
app:rippleColor="@color/btn_nav_left_ripple"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" />
app:layout_constraintHorizontal_bias="0.056"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.976" />
<Button <!-- Fertig/Speichern (rechts unten) gleiche Farbe wie Qprev -->
<com.google.android.material.button.MaterialButton
android:id="@+id/Qfinish" android:id="@+id/Qfinish"
android:layout_width="231dp" android:layout_width="@dimen/finish_btn_width"
android:layout_height="42dp" android:layout_height="@dimen/nav_btn_size"
android:tag="finish" android:layout_marginEnd="20dp"
android:layout_marginBottom="16dp"
android:textAllCaps="false"
android:minWidth="0dp"
android:insetLeft="0dp"
android:insetRight="0dp"
android:paddingStart="24dp"
android:paddingEnd="24dp"
app:cornerRadius="999dp"
app:backgroundTint="@color/btn_nav_right_tint"
app:rippleColor="@color/btn_nav_right_ripple"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent" />
app:layout_constraintHorizontal_bias="0.944"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.976" />
<TextView <TextView
android:id="@+id/textView" android:id="@+id/textView"
android:layout_width="389dp" android:layout_width="0dp"
android:layout_height="115dp" android:layout_height="0dp"
android:tag="finish_data_entry" android:gravity="start|center_vertical"
android:textAlignment="viewStart" android:textAlignment="viewStart"
android:textSize="24sp" android:paddingStart="8dp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.25"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.125" /> app:layout_constraintVertical_bias="0.051"
app:layout_constraintWidth_percent="0.9" />
<TextView
android:id="@+id/question"
android:layout_width="0dp"
android:layout_height="0dp"
android:gravity="start|center_vertical"
android:textAlignment="viewStart"
android:paddingStart="8dp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.25"
app:layout_constraintHorizontal_bias="0.512"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView"
app:layout_constraintVertical_bias="0.0"
app:layout_constraintWidth_percent="0.9" />
<!-- ProgressBar for loading animation -->
<ProgressBar <ProgressBar
android:id="@+id/progressBar" android:id="@+id/progressBar"
style="?android:attr/progressBarStyleLarge" style="?android:attr/progressBarStyleLarge"

View File

@ -5,71 +5,91 @@
android:id="@+id/main" android:id="@+id/main"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".MainActivity" tools:context=".MainActivity">
tools:layout_editor_absoluteX="11dp"
tools:layout_editor_absoluteY="107dp">
<Button <androidx.constraintlayout.widget.Guideline
android:id="@+id/gTop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_begin="32dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/Qprev" android:id="@+id/Qprev"
android:tag="previous" android:layout_width="@dimen/nav_btn_size"
android:layout_width="130dp" android:layout_height="@dimen/nav_btn_size"
android:layout_height="42dp" android:layout_marginStart="20dp"
android:layout_marginBottom="16dp"
android:text=""
android:textAllCaps="false"
app:icon="@drawable/ic_chevron_left"
app:iconTint="@color/btn_nav_left_icon_tint"
app:iconSize="@dimen/nav_icon_size"
app:iconPadding="0dp"
app:cornerRadius="999dp"
app:backgroundTint="@color/btn_nav_left_tint"
app:rippleColor="@color/btn_nav_left_ripple"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" />
app:layout_constraintHorizontal_bias="0.056"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.976" />
<Button <com.google.android.material.button.MaterialButton
android:id="@+id/Qnext" android:id="@+id/Qnext"
android:tag="next" android:layout_width="@dimen/nav_btn_size"
android:layout_width="130dp" android:layout_height="@dimen/nav_btn_size"
android:layout_height="42dp" android:layout_marginEnd="20dp"
android:layout_marginBottom="16dp"
android:text=""
android:textAllCaps="false"
app:icon="@drawable/ic_chevron_right"
app:iconTint="@color/btn_nav_right_icon_tint"
app:iconSize="@dimen/nav_icon_size"
app:iconPadding="0dp"
app:cornerRadius="999dp"
app:backgroundTint="@color/btn_nav_right_tint"
app:rippleColor="@color/btn_nav_right_ripple"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent" />
app:layout_constraintHorizontal_bias="0.943"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.976" />
<TextView <TextView
android:id="@+id/textView" android:id="@+id/textView"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAlignment="center" android:gravity="center"
android:layout_marginStart="25dp" android:textStyle="bold"
android:layout_marginEnd="25dp" android:paddingStart="16dp"
app:layout_constraintBottom_toBottomOf="parent" android:paddingEnd="16dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/gTop"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintVertical_bias="0.104" /> app:layout_constraintWidth_percent="0.9" />
<TextView <TextView
android:id="@+id/question" android:id="@+id/question"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAlignment="center" android:gravity="center"
android:textStyle="bold" android:textStyle="bold"
android:layout_marginStart="25dp" android:paddingStart="16dp"
android:layout_marginEnd="25dp" android:paddingEnd="16dp"
app:layout_constraintBottom_toBottomOf="parent" android:paddingTop="8dp"
app:layout_constraintEnd_toEndOf="parent" android:paddingBottom="8dp"
app:layout_constraintTop_toBottomOf="@id/textView"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintVertical_bias="0.188" /> app:layout_constraintWidth_percent="0.9" />
<ScrollView <ScrollView
android:id="@+id/scrollView" android:id="@+id/scrollView"
android:layout_width="360dp" android:layout_width="0dp"
android:layout_height="417dp" android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent" android:fillViewport="true"
app:layout_constraintEnd_toEndOf="parent" android:layout_marginTop="8dp"
app:layout_constraintHorizontal_bias="0.489" android:layout_marginBottom="8dp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintVertical_bias="0.658"> app:layout_constraintTop_toBottomOf="@id/question"
app:layout_constraintBottom_toTopOf="@id/Qnext"
app:layout_constraintWidth_percent="0.9">
<LinearLayout <LinearLayout
android:id="@+id/CheckboxContainer" android:id="@+id/CheckboxContainer"
@ -78,4 +98,5 @@
android:orientation="vertical" android:orientation="vertical"
android:padding="16dp" /> android:padding="16dp" />
</ScrollView> </ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -3,100 +3,307 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:padding="16dp">
<EditText <com.google.android.material.card.MaterialCardView
android:id="@+id/editText" android:id="@+id/headerCard"
android:layout_width="216dp" android:layout_width="0dp"
android:layout_height="53dp" android:layout_height="wrap_content"
android:background="@android:drawable/edit_text" android:layout_marginTop="32dp"
android:ems="10" android:paddingStart="24dp"
android:inputType="text" android:paddingEnd="24dp"
android:tag="client_code" android:paddingTop="20dp"
app:layout_constraintBottom_toBottomOf="parent" android:paddingBottom="20dp"
app:layout_constraintEnd_toEndOf="parent" app:cardBackgroundColor="@color/brand_surface"
app:layout_constraintHorizontal_bias="0.897" app:cardCornerRadius="16dp"
app:layout_constraintStart_toStartOf="parent" app:strokeColor="@color/brand_stroke"
app:strokeWidth="1dp"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.091" /> app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- Language (eigene Zeile) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="6dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingBottom="6dp"
android:text="Language"
android:textColor="@color/brand_text_dark"
android:textSize="12sp" />
<Spinner <Spinner
android:id="@+id/string_spinner1" android:id="@+id/string_spinner1"
android:layout_width="143dp" android:layout_width="match_parent"
android:layout_height="43dp" android:layout_height="48dp"
app:layout_constraintBottom_toBottomOf="parent" android:layout_marginStart="8dp"
app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="8dp"
app:layout_constraintHorizontal_bias="0.07" android:background="@drawable/bg_field_filled"
app:layout_constraintStart_toStartOf="parent" android:paddingStart="12dp"
app:layout_constraintTop_toTopOf="parent" android:paddingEnd="12dp" />
app:layout_constraintVertical_bias="0.091" /> </LinearLayout>
<!-- Container für dynamische Buttons --> <!-- Client Code | Coach Code (gemeinsame Zeile) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:baselineAligned="false"
android:paddingBottom="6dp">
<LinearLayout <LinearLayout
android:id="@+id/buttonContainer"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="28dp" android:layout_weight="1"
android:orientation="vertical" android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="8dp">
app:layout_constraintHorizontal_bias="0.0"
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingBottom="6dp"
android:text="Client Code"
android:textColor="@color/brand_text_dark"
android:textSize="12sp" />
<EditText
android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:background="@drawable/bg_field_filled"
android:ems="10"
android:inputType="text"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:tag="client_code"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginStart="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingBottom="6dp"
android:text="Coach Code"
android:textColor="@color/brand_text_dark"
android:textSize="12sp" />
<EditText
android:id="@+id/coachEditText"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:background="@drawable/bg_field_locked"
android:ems="10"
android:inputType="none"
android:focusable="false"
android:focusableInTouchMode="false"
android:clickable="false"
android:cursorVisible="false"
android:longClickable="false"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:drawableEnd="@drawable/ic_lock_24"
android:drawablePadding="8dp"
android:textColor="@color/brand_text_dark"
android:textStyle="bold"
android:tag="coach_code"/>
</LinearLayout>
</LinearLayout>
<!-- Session/Online -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingTop="6dp">
<TextView
android:id="@+id/statusSession"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:text="Session: —"
android:textColor="@color/brand_text_dark"
android:textSize="13sp" />
<TextView
android:id="@+id/statusOnline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Offline"
android:textStyle="bold"
android:layout_marginEnd="8dp"
android:textSize="13sp"
android:textColor="#C62828"
android:paddingStart="12dp"/>
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<LinearLayout
android:id="@+id/primaryActionsRow"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:baselineAligned="false"
android:layout_marginTop="20dp"
app:layout_constraintTop_toBottomOf="@id/headerCard"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textView" /> app:layout_constraintEnd_toEndOf="parent">
<Button <com.google.android.material.button.MaterialButton
android:id="@+id/loadButton" android:id="@+id/loadButton"
android:layout_width="72dp" android:layout_width="0dp"
android:layout_height="52dp" android:layout_height="@dimen/pill_height"
app:layout_constraintEnd_toEndOf="@id/editText" android:layout_weight="1"
app:layout_constraintHorizontal_bias="0.0" android:layout_marginEnd="12dp"
app:layout_constraintStart_toStartOf="@id/editText" android:textAllCaps="false"
app:layout_constraintTop_toBottomOf="@id/editText" /> android:textColor="@android:color/white"
app:icon="@drawable/ic_dot_16"
app:iconTint="@android:color/white"
app:iconPadding="8dp"
app:iconGravity="textStart"
app:cornerRadius="@dimen/pill_radius"
app:backgroundTint="@color/brand_purple"/>
<Button <com.google.android.material.button.MaterialButton
android:id="@+id/uploadButton"
android:layout_width="200dp"
android:layout_height="42dp"
android:layout_marginTop="52dp"
app:layout_constraintEnd_toEndOf="@id/editText"
app:layout_constraintStart_toStartOf="@id/editText"
app:layout_constraintTop_toBottomOf="@id/editText" />
<Button
android:id="@+id/downloadButton"
android:layout_width="200dp"
android:layout_height="42dp"
android:layout_marginTop="92dp"
app:layout_constraintEnd_toEndOf="@id/editText"
app:layout_constraintStart_toStartOf="@id/editText"
app:layout_constraintTop_toBottomOf="@id/editText" />
<Button
android:id="@+id/editButton" android:id="@+id/editButton"
android:layout_width="72dp" android:layout_width="0dp"
android:layout_height="52dp" android:layout_height="@dimen/pill_height"
app:layout_constraintEnd_toEndOf="@id/editText" android:layout_weight="1"
app:layout_constraintStart_toStartOf="@id/editText" android:layout_marginEnd="12dp"
app:layout_constraintTop_toBottomOf="@id/editText" /> android:textAllCaps="false"
android:textColor="@android:color/white"
app:icon="@drawable/ic_dot_16"
app:iconTint="@android:color/white"
app:iconPadding="8dp"
app:iconGravity="textStart"
app:cornerRadius="@dimen/pill_radius"
app:backgroundTint="@color/brand_purple"/>
<Button <com.google.android.material.button.MaterialButton
android:id="@+id/saveButton" android:id="@+id/saveButton"
android:layout_width="72dp" android:layout_width="0dp"
android:layout_height="52dp" android:layout_height="@dimen/pill_height"
app:layout_constraintEnd_toEndOf="@id/editText" android:layout_weight="1"
app:layout_constraintHorizontal_bias="1.0" android:textAllCaps="false"
app:layout_constraintStart_toStartOf="@id/editText" android:textColor="@android:color/white"
app:layout_constraintTop_toBottomOf="@id/editText" /> app:icon="@drawable/ic_dot_16"
app:iconTint="@android:color/white"
app:iconPadding="8dp"
app:iconGravity="textStart"
app:cornerRadius="@dimen/pill_radius"
app:backgroundTint="@color/brand_purple"/>
</LinearLayout>
<LinearLayout
android:id="@+id/secondaryActionsColumn"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@id/primaryActionsRow"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<com.google.android.material.button.MaterialButton
android:id="@+id/uploadButton"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="@dimen/pill_height"
android:layout_marginBottom="12dp"
android:textAllCaps="false"
android:textColor="@color/brand_purple"
app:cornerRadius="@dimen/pill_radius"
app:strokeColor="@color/brand_purple"
app:strokeWidth="@dimen/pill_stroke"
app:backgroundTint="@android:color/transparent"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/downloadButton"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="@dimen/pill_height"
android:layout_marginBottom="12dp"
android:textAllCaps="false"
android:textColor="@color/brand_purple"
app:cornerRadius="@dimen/pill_radius"
app:strokeColor="@color/brand_purple"
app:strokeWidth="@dimen/pill_stroke"
app:backgroundTint="@android:color/transparent"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/databaseButton"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="@dimen/pill_height"
android:textAllCaps="false"
android:textColor="@color/brand_purple"
app:cornerRadius="@dimen/pill_radius"
app:strokeColor="@color/brand_purple"
app:strokeWidth="@dimen/pill_stroke"
app:backgroundTint="@android:color/transparent"/>
</LinearLayout>
<TextView <TextView
android:id="@+id/textView" android:id="@+id/textView"
android:layout_width="348dp" android:layout_width="0dp"
android:layout_height="162dp" android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent" android:layout_marginTop="24dp"
app:layout_constraintEnd_toEndOf="parent" android:textStyle="bold"
app:layout_constraintHorizontal_bias="0.492" android:textSize="22sp"
android:textColor="@color/brand_text_dark"
app:layout_constraintTop_toBottomOf="@id/secondaryActionsColumn"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toEndOf="parent"/>
app:layout_constraintVertical_bias="0.307" />
<androidx.core.widget.NestedScrollView
android:id="@+id/questionnaireScroll"
android:layout_width="0dp"
android:layout_height="0dp"
android:fillViewport="true"
android:clipToPadding="false"
android:paddingBottom="16dp"
app:layout_constraintTop_toBottomOf="@id/textView"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<LinearLayout
android:id="@+id/buttonContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="8dp"/>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rootQuestionnaireDetail"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/titleQuestionnaireDetail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Fragen und Antworten"
android:textStyle="bold"
android:textSize="20sp"
android:paddingBottom="8dp" />
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:fillViewport="true"
android:scrollbars="horizontal">
<ScrollView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:fillViewport="true"
android:scrollbars="vertical">
<TableLayout
android:id="@+id/tableQA"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:stretchColumns="1,2" />
</ScrollView>
</HorizontalScrollView>
<TextView
android:id="@+id/emptyViewQA"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Keine Fragen vorhanden."
android:visibility="gone"
android:paddingTop="8dp" />
<ProgressBar
android:id="@+id/progressBarQA"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:paddingTop="8dp"
android:paddingBottom="8dp" />
<Button
android:id="@+id/backButtonQA"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Zurück" />
</LinearLayout>

View File

@ -5,70 +5,99 @@
android:id="@+id/main" android:id="@+id/main"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".MainActivity" tools:context=".MainActivity">
tools:layout_editor_absoluteX="11dp"
tools:layout_editor_absoluteY="107dp">
<Button <androidx.constraintlayout.widget.Guideline
android:id="@+id/gTop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_begin="32dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/Qprev" android:id="@+id/Qprev"
android:tag="previous" android:layout_width="@dimen/nav_btn_size"
android:layout_width="130dp" android:layout_height="@dimen/nav_btn_size"
android:layout_height="42dp" android:layout_marginStart="20dp"
android:layout_marginBottom="16dp"
android:text=""
android:textAllCaps="false"
app:icon="@drawable/ic_chevron_left"
app:iconTint="@color/btn_nav_left_icon_tint"
app:iconSize="@dimen/nav_icon_size"
app:iconPadding="0dp"
app:cornerRadius="999dp"
app:backgroundTint="@color/btn_nav_left_tint"
app:rippleColor="@color/btn_nav_left_ripple"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" />
app:layout_constraintHorizontal_bias="0.056"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.976" />
<Button <com.google.android.material.button.MaterialButton
android:id="@+id/Qnext" android:id="@+id/Qnext"
android:tag="next" android:layout_width="@dimen/nav_btn_size"
android:layout_width="130dp" android:layout_height="@dimen/nav_btn_size"
android:layout_height="42dp" android:layout_marginEnd="20dp"
android:layout_marginBottom="16dp"
android:text=""
android:textAllCaps="false"
app:icon="@drawable/ic_chevron_right"
app:iconTint="@color/btn_nav_right_icon_tint"
app:iconSize="@dimen/nav_icon_size"
app:iconPadding="0dp"
app:cornerRadius="999dp"
app:backgroundTint="@color/btn_nav_right_tint"
app:rippleColor="@color/btn_nav_right_ripple"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent" />
app:layout_constraintHorizontal_bias="0.943"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.976" />
<RadioGroup
android:id="@+id/RadioGroup"
android:layout_width="323dp"
android:layout_height="419dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.675" />
<TextView <TextView
android:id="@+id/textView" android:id="@+id/textView"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAlignment="center" android:gravity="center"
android:layout_marginStart="25dp" android:textStyle="bold"
android:layout_marginEnd="25dp" android:paddingStart="16dp"
app:layout_constraintBottom_toBottomOf="parent" android:paddingEnd="16dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/gTop"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintVertical_bias="0.104" /> app:layout_constraintWidth_percent="0.9" />
<TextView <TextView
android:id="@+id/question" android:id="@+id/question"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAlignment="center" android:gravity="center"
android:textStyle="bold" android:textStyle="bold"
android:layout_marginStart="25dp" android:paddingStart="16dp"
android:layout_marginEnd="25dp" android:paddingEnd="16dp"
app:layout_constraintBottom_toBottomOf="parent" android:paddingTop="8dp"
app:layout_constraintEnd_toEndOf="parent" android:paddingBottom="8dp"
app:layout_constraintTop_toBottomOf="@id/textView"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintVertical_bias="0.188" /> app:layout_constraintWidth_percent="0.9" />
<ScrollView
android:id="@+id/radioScroll"
android:layout_width="0dp"
android:layout_height="0dp"
android:fillViewport="true"
android:clipToPadding="false"
android:paddingTop="8dp"
android:paddingBottom="8dp"
app:layout_constraintTop_toBottomOf="@id/question"
app:layout_constraintBottom_toTopOf="@id/Qnext"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintWidth_percent="0.80">
<RadioGroup
android:id="@+id/RadioGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp" />
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -5,68 +5,85 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/gTop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_begin="32dp" />
<Spinner <Spinner
android:id="@+id/string_spinner" android:id="@+id/string_spinner"
android:layout_width="250dp" android:layout_width="0dp"
android:layout_height="35dp" android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent" android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintWidth_percent="0.70"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.38" />
<Button
android:id="@+id/Qnext"
android:layout_width="130dp"
android:layout_height="42dp"
android:tag="next"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.943" app:layout_constraintTop_toBottomOf="@id/question" />
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.976" />
<Button <com.google.android.material.button.MaterialButton
android:id="@+id/Qprev" android:id="@+id/Qprev"
android:layout_width="130dp" android:layout_width="@dimen/nav_btn_size"
android:layout_height="42dp" android:layout_height="@dimen/nav_btn_size"
android:tag="previous" android:layout_marginStart="20dp"
android:layout_marginBottom="16dp"
android:text=""
android:textAllCaps="false"
app:icon="@drawable/ic_chevron_left"
app:iconTint="@color/btn_nav_left_icon_tint"
app:iconSize="@dimen/nav_icon_size"
app:iconPadding="0dp"
app:cornerRadius="999dp"
app:backgroundTint="@color/btn_nav_left_tint"
app:rippleColor="@color/btn_nav_left_ripple"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" />
app:layout_constraintHorizontal_bias="0.056"
app:layout_constraintStart_toStartOf="parent" <com.google.android.material.button.MaterialButton
app:layout_constraintTop_toTopOf="parent" android:id="@+id/Qnext"
app:layout_constraintVertical_bias="0.976" /> android:layout_width="@dimen/nav_btn_size"
android:layout_height="@dimen/nav_btn_size"
android:layout_marginEnd="20dp"
android:layout_marginBottom="16dp"
android:text=""
android:textAllCaps="false"
app:icon="@drawable/ic_chevron_right"
app:iconTint="@color/btn_nav_right_icon_tint"
app:iconSize="@dimen/nav_icon_size"
app:iconPadding="0dp"
app:cornerRadius="999dp"
app:backgroundTint="@color/btn_nav_right_tint"
app:rippleColor="@color/btn_nav_right_ripple"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<TextView <TextView
android:id="@+id/textView" android:id="@+id/textView"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAlignment="center" android:gravity="center"
android:layout_marginStart="25dp" android:textStyle="bold"
android:layout_marginEnd="25dp" android:paddingStart="16dp"
app:layout_constraintBottom_toBottomOf="parent" android:paddingEnd="16dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/gTop"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintVertical_bias="0.104" /> app:layout_constraintWidth_percent="0.9" />
<TextView <TextView
android:id="@+id/question" android:id="@+id/question"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAlignment="center" android:gravity="center"
android:textStyle="bold" android:textStyle="bold"
android:layout_marginStart="25dp" android:paddingStart="16dp"
android:layout_marginEnd="25dp" android:paddingEnd="16dp"
app:layout_constraintBottom_toBottomOf="parent" android:paddingTop="8dp"
app:layout_constraintEnd_toEndOf="parent" android:paddingBottom="8dp"
app:layout_constraintTop_toBottomOf="@id/textView"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintVertical_bias="0.188" /> app:layout_constraintWidth_percent="0.9" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -5,67 +5,87 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/gTop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_begin="32dp" />
<Spinner <Spinner
android:id="@+id/value_spinner" android:id="@+id/value_spinner"
android:layout_width="250dp" android:layout_width="0dp"
android:layout_height="35dp" android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent" android:layout_marginTop="8dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498" app:layout_constraintHorizontal_bias="0.495"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toBottomOf="@id/question"
app:layout_constraintVertical_bias="0.38" /> app:layout_constraintVertical_bias="0.027"
app:layout_constraintWidth_percent="0.70" />
<Button <com.google.android.material.button.MaterialButton
android:id="@+id/Qnext"
android:layout_width="130dp"
android:layout_height="42dp"
android:tag="next"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.943"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.976" />
<Button
android:id="@+id/Qprev" android:id="@+id/Qprev"
android:layout_width="130dp" android:layout_width="@dimen/nav_btn_size"
android:layout_height="42dp" android:layout_height="@dimen/nav_btn_size"
android:tag="previous" android:layout_marginStart="20dp"
android:layout_marginBottom="16dp"
android:text=""
android:textAllCaps="false"
app:icon="@drawable/ic_chevron_left"
app:iconTint="@color/btn_nav_left_icon_tint"
app:iconSize="@dimen/nav_icon_size"
app:iconPadding="0dp"
app:cornerRadius="999dp"
app:backgroundTint="@color/btn_nav_left_tint"
app:rippleColor="@color/btn_nav_left_ripple"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" />
app:layout_constraintHorizontal_bias="0.056"
app:layout_constraintStart_toStartOf="parent" <com.google.android.material.button.MaterialButton
app:layout_constraintTop_toTopOf="parent" android:id="@+id/Qnext"
app:layout_constraintVertical_bias="0.976" /> android:layout_width="@dimen/nav_btn_size"
android:layout_height="@dimen/nav_btn_size"
android:layout_marginEnd="20dp"
android:layout_marginBottom="16dp"
android:text=""
android:textAllCaps="false"
app:icon="@drawable/ic_chevron_right"
app:iconTint="@color/btn_nav_right_icon_tint"
app:iconSize="@dimen/nav_icon_size"
app:iconPadding="0dp"
app:cornerRadius="999dp"
app:backgroundTint="@color/btn_nav_right_tint"
app:rippleColor="@color/btn_nav_right_ripple"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<TextView <TextView
android:id="@+id/textView" android:id="@+id/textView"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAlignment="center" android:gravity="center"
android:layout_marginStart="25dp" android:textStyle="bold"
android:layout_marginEnd="25dp" android:paddingStart="16dp"
app:layout_constraintBottom_toBottomOf="parent" android:paddingEnd="16dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/gTop"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintVertical_bias="0.104" /> app:layout_constraintWidth_percent="0.9" />
<TextView <TextView
android:id="@+id/question" android:id="@+id/question"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAlignment="center" android:gravity="center"
android:textStyle="bold" android:textStyle="bold"
android:layout_marginStart="25dp" android:paddingStart="16dp"
android:layout_marginEnd="25dp" android:paddingEnd="16dp"
app:layout_constraintBottom_toBottomOf="parent" android:paddingTop="8dp"
app:layout_constraintEnd_toEndOf="parent" android:paddingBottom="8dp"
app:layout_constraintTop_toBottomOf="@id/textView"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintVertical_bias="0.188" /> app:layout_constraintWidth_percent="0.9" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -2,4 +2,8 @@
<resources> <resources>
<color name="black">#FF000000</color> <color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color> <color name="white">#FFFFFFFF</color>
<color name="brand_purple">#6E56CF</color>
<color name="brand_text_dark">#2D2748</color>
<color name="brand_surface">#FFFFFFFF</color>
<color name="brand_stroke">#D8D1F0</color>
</resources> </resources>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="nav_btn_size">64dp</dimen>
<dimen name="nav_icon_size">28dp</dimen>
<dimen name="finish_btn_width">200dp</dimen>
<dimen name="pill_height">48dp</dimen>
<dimen name="pill_radius">28dp</dimen>
<dimen name="pill_stroke">2dp</dimen>
</resources>