Compare commits
49 Commits
148af18496
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| d30c94beeb | |||
| 5b1264293c | |||
| 39a4811fd2 | |||
| 8b3bb358e8 | |||
| 5968bf68d1 | |||
| ad09bce68c | |||
| 4089841336 | |||
| 5570710da5 | |||
| 8d54315fe7 | |||
| ac2e0dabd2 | |||
| 66122dd6c3 | |||
| dcfa261c1c | |||
| 851676f6c3 | |||
| cfcb689ffc | |||
| bf33501b69 | |||
| 5f5c766133 | |||
| 91f6f77b73 | |||
| 8dc9be20a4 | |||
| 31e2abecf8 | |||
| 894823f42a | |||
| 1ffd09049e | |||
| 0a04568a7c | |||
| ca8f6ca8e4 | |||
| e1cf8b4926 | |||
| 8ad939db27 | |||
| ac1fbb515d | |||
| fe2b05c0fd | |||
| 7014386953 | |||
| 4cf840b37a | |||
| af9c045341 | |||
| 77742275e6 | |||
| 93d2fa4333 | |||
| 67c11720b9 | |||
| 304cabc0d7 | |||
| 0dfc5df878 | |||
| 650a3bb050 | |||
| 45deee664b | |||
| 6aebda0009 | |||
| 90e662a330 | |||
| ed4d747798 | |||
| 0992304e59 | |||
| 073f33a9bb | |||
| 5f568f4c0e | |||
| a803be05d5 | |||
| 0a70cb78fd | |||
| ecb1f9b1a2 | |||
| 4e9338631b | |||
| 95f290b46a | |||
| a1736d0241 |
4
.idea/deploymentTargetSelector.xml
generated
4
.idea/deploymentTargetSelector.xml
generated
@ -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>
|
||||||
|
|||||||
8
.idea/misc.xml
generated
8
.idea/misc.xml
generated
@ -1,3 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||||
@ -6,4 +7,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
6
.idea/render.experimental.xml
generated
Normal 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>
|
||||||
@ -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
BIN
app/release/app-release.apk
Normal file
Binary file not shown.
BIN
app/release/baselineProfiles/0/app-release.dm
Normal file
BIN
app/release/baselineProfiles/0/app-release.dm
Normal file
Binary file not shown.
BIN
app/release/baselineProfiles/1/app-release.dm
Normal file
BIN
app/release/baselineProfiles/1/app-release.dm
Normal file
Binary file not shown.
37
app/release/output-metadata.json
Normal file
37
app/release/output-metadata.json
Normal 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
|
||||||
|
}
|
||||||
@ -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" />
|
||||||
|
|||||||
BIN
app/src/main/assets/header_order.xlsx
Normal file
BIN
app/src/main/assets/header_order.xlsx
Normal file
Binary file not shown.
@ -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",
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -28,7 +28,11 @@
|
|||||||
"questionnaire_1_demographic_information",
|
"questionnaire_1_demographic_information",
|
||||||
"questionnaire_2_rhs",
|
"questionnaire_2_rhs",
|
||||||
"questionnaire_3_integration_index"
|
"questionnaire_3_integration_index"
|
||||||
]
|
],
|
||||||
|
"questionnaire": "questionnaire_1_demographic_information",
|
||||||
|
"questionId": "consent_instruction",
|
||||||
|
"operator": "==",
|
||||||
|
"value": "consent_signed"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -44,7 +48,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "questionnaire_6_follow_up_survey.json",
|
"file": "questionnaire_6_follow_up_survey.json",
|
||||||
"showPoints": false,
|
"showPoints": true,
|
||||||
"condition": {
|
"condition": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,56 +1,71 @@
|
|||||||
|
package com.dano.test1
|
||||||
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.security.SecureRandom
|
||||||
import java.io.FileOutputStream
|
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
import javax.crypto.CipherInputStream
|
import javax.crypto.Mac
|
||||||
import javax.crypto.CipherOutputStream
|
|
||||||
import javax.crypto.spec.IvParameterSpec
|
import javax.crypto.spec.IvParameterSpec
|
||||||
import javax.crypto.spec.SecretKeySpec
|
import javax.crypto.spec.SecretKeySpec
|
||||||
import kotlin.random.Random
|
import kotlin.math.min
|
||||||
|
|
||||||
object AES256Helper {
|
object AES256Helper {
|
||||||
|
|
||||||
private const val TRANSFORMATION = "AES/CBC/PKCS5Padding"
|
private fun hkdfFromToken(tokenHex: String, info: String = "qdb-aes", len: Int = 32): ByteArray {
|
||||||
private const val ALGORITHM = "AES"
|
val ikm = hexToBytes(tokenHex)
|
||||||
private const val IV_SIZE = 16
|
val mac = Mac.getInstance("HmacSHA256")
|
||||||
|
val zeroSalt = ByteArray(32) { 0 }
|
||||||
|
mac.init(SecretKeySpec(zeroSalt, "HmacSHA256"))
|
||||||
|
val prk = mac.doFinal(ikm)
|
||||||
|
|
||||||
// Beispiel-Key: 32 Bytes = 256 bit. Ersetze das durch deinen eigenen sicheren Schlüssel!
|
var previous = ByteArray(0)
|
||||||
private val keyBytes = "12345678901234567890123456789012".toByteArray(Charsets.UTF_8)
|
val okm = ByteArray(len)
|
||||||
private val secretKey = SecretKeySpec(keyBytes, ALGORITHM)
|
var generated = 0
|
||||||
|
var counter = 1
|
||||||
// Verschlüsseln: InputFile -> OutputFile (mit zufälligem IV vorne in der Datei)
|
while (generated < len) {
|
||||||
fun encryptFile(inputFile: File, outputFile: File) {
|
mac.init(SecretKeySpec(prk, "HmacSHA256"))
|
||||||
val iv = ByteArray(IV_SIZE)
|
mac.update(previous)
|
||||||
Random.nextBytes(iv)
|
mac.update(info.toByteArray(Charsets.UTF_8))
|
||||||
val ivSpec = IvParameterSpec(iv)
|
mac.update(counter.toByte())
|
||||||
val cipher = Cipher.getInstance(TRANSFORMATION)
|
val t = mac.doFinal()
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec)
|
val toCopy = min(len - generated, t.size)
|
||||||
|
System.arraycopy(t, 0, okm, generated, toCopy)
|
||||||
FileOutputStream(outputFile).use { fileOut ->
|
previous = t
|
||||||
// IV vorne reinschreiben
|
generated += toCopy
|
||||||
fileOut.write(iv)
|
counter++
|
||||||
CipherOutputStream(fileOut, cipher).use { cipherOut ->
|
|
||||||
FileInputStream(inputFile).use { fileIn ->
|
|
||||||
fileIn.copyTo(cipherOut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return okm
|
||||||
}
|
}
|
||||||
|
|
||||||
// Entschlüsseln: InputFile (IV+Ciphertext) -> OutputFile (Klartext)
|
private fun hexToBytes(hex: String): ByteArray {
|
||||||
fun decryptFile(inputFile: File, outputFile: File) {
|
val clean = hex.trim()
|
||||||
FileInputStream(inputFile).use { fileIn ->
|
val len = clean.length
|
||||||
val iv = ByteArray(IV_SIZE)
|
val out = ByteArray(len / 2)
|
||||||
if (fileIn.read(iv) != IV_SIZE) throw IllegalArgumentException("Ungültige Datei oder IV fehlt")
|
var i = 0
|
||||||
val ivSpec = IvParameterSpec(iv)
|
while (i < len) {
|
||||||
val cipher = Cipher.getInstance(TRANSFORMATION)
|
out[i / 2] = ((Character.digit(clean[i], 16) shl 4) + Character.digit(clean[i + 1], 16)).toByte()
|
||||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec)
|
i += 2
|
||||||
|
|
||||||
CipherInputStream(fileIn, cipher).use { cipherIn ->
|
|
||||||
FileOutputStream(outputFile).use { fileOut ->
|
|
||||||
cipherIn.copyTo(fileOut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
465
app/src/main/java/com/dano/test1/DatabaseButtonHandler.kt
Normal file
465
app/src/main/java/com/dano/test1/DatabaseButtonHandler.kt
Normal file
@ -0,0 +1,465 @@
|
|||||||
|
package com.dano.test1
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.*
|
||||||
|
import com.dano.test1.data.Client
|
||||||
|
import com.dano.test1.data.Question
|
||||||
|
import com.dano.test1.data.Questionnaire
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
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(android.graphics.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, android.graphics.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 {
|
||||||
|
val density = activity.resources.displayMetrics.density
|
||||||
|
return (value * density).roundToInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,77 +5,58 @@ import android.util.Log
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import javax.crypto.Cipher
|
|
||||||
import javax.crypto.spec.IvParameterSpec
|
|
||||||
import javax.crypto.spec.SecretKeySpec
|
|
||||||
|
|
||||||
object DatabaseDownloader {
|
object DatabaseDownloader {
|
||||||
|
|
||||||
private const val DB_NAME = "questionnaire_database"
|
private const val DB_NAME = "questionnaire_database"
|
||||||
private const val API_TOKEN = "MEIN_SUPER_GEHEIMES_TOKEN_12345"
|
private const val SERVER_DOWNLOAD_URL = "https://daniel-ocks.de/qdb/downloadFull.php"
|
||||||
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()
|
private val client = OkHttpClient()
|
||||||
|
|
||||||
fun downloadAndReplaceDatabase(context: Context) {
|
// Neue Variante mit Callback
|
||||||
|
fun downloadAndReplaceDatabase(context: Context, token: String, onDone: ((Boolean) -> Unit)? = null) {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
var ok = false
|
||||||
try {
|
try {
|
||||||
Log.d("DOWNLOAD", "Download gestartet: $SERVER_DOWNLOAD_URL")
|
|
||||||
|
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url(SERVER_DOWNLOAD_URL)
|
.url(SERVER_DOWNLOAD_URL)
|
||||||
|
.header("Authorization", "Bearer $token")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val response = client.newCall(request).execute()
|
val response = client.newCall(request).execute()
|
||||||
if (!response.isSuccessful) {
|
if (!response.isSuccessful) {
|
||||||
Log.e("DOWNLOAD", "Fehler beim Download: ${response.code}")
|
Log.e("DOWNLOAD", "HTTP ${response.code}")
|
||||||
|
withContext(Dispatchers.Main) { onDone?.invoke(false) }
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zwischenspeichern der verschlüsselten Datei
|
val encFile = File(context.cacheDir, "downloaded_database.enc")
|
||||||
val downloadedFile = File(context.cacheDir, "downloaded_database.enc")
|
|
||||||
response.body?.byteStream()?.use { input ->
|
response.body?.byteStream()?.use { input ->
|
||||||
FileOutputStream(downloadedFile).use { output ->
|
FileOutputStream(encFile).use { output -> input.copyTo(output) }
|
||||||
input.copyTo(output)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Log.d("DOWNLOAD", "Datei gespeichert: ${downloadedFile.absolutePath}")
|
|
||||||
|
|
||||||
// Entschlüsselung
|
val decryptedBytes = AES256Helper.decryptFileWithToken(encFile, token)
|
||||||
val decryptedBytes = decryptFile(downloadedFile)
|
|
||||||
val dbFile = context.getDatabasePath(DB_NAME)
|
val dbFile = context.getDatabasePath(DB_NAME)
|
||||||
if (dbFile.exists()) dbFile.delete()
|
if (dbFile.exists()) dbFile.delete()
|
||||||
FileOutputStream(dbFile).use { fos ->
|
FileOutputStream(dbFile).use { it.write(decryptedBytes) }
|
||||||
fos.write(decryptedBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d("DOWNLOAD", "Neue DB erfolgreich entschlüsselt und eingesetzt")
|
|
||||||
|
|
||||||
|
Log.d("DOWNLOAD", "DB erfolgreich ersetzt")
|
||||||
|
ok = true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("DOWNLOAD", "Fehler beim Download oder Ersetzen der DB", e)
|
Log.e("DOWNLOAD", "Fehler", e)
|
||||||
|
} finally {
|
||||||
|
withContext(Dispatchers.Main) { onDone?.invoke(ok) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun decryptFile(file: File): ByteArray {
|
// Abwärtskompatible alte Signatur
|
||||||
val fileBytes = file.readBytes()
|
fun downloadAndReplaceDatabase(context: Context, token: String) {
|
||||||
if (fileBytes.size < 16) throw IllegalArgumentException("Datei zu kurz, kein IV vorhanden")
|
downloadAndReplaceDatabase(context, token, null)
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,95 +20,49 @@ import kotlin.system.exitProcess
|
|||||||
object DatabaseUploader {
|
object DatabaseUploader {
|
||||||
|
|
||||||
private const val DB_NAME = "questionnaire_database"
|
private const val DB_NAME = "questionnaire_database"
|
||||||
// TODO entferne uploadDeltaTest2.php
|
private const val SERVER_DELTA_URL = "https://daniel-ocks.de/qdb/uploadDeltaTest5.php"
|
||||||
private const val SERVER_DELTA_URL = "http://49.13.157.44/uploadDeltaTest3.php"
|
private const val SERVER_CHECK_URL = "https://daniel-ocks.de/qdb/checkDatabaseExists.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()
|
private val client = OkHttpClient()
|
||||||
|
|
||||||
fun uploadDatabase(context: Context) {
|
private fun uploadDatabase(context: Context, token: String) {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
try {
|
try {
|
||||||
val dbFile = context.getDatabasePath(DB_NAME)
|
val dbFile = context.getDatabasePath(DB_NAME)
|
||||||
if (!dbFile.exists()) {
|
if (!dbFile.exists()) {
|
||||||
Log.e("UPLOAD", "Datenbankdatei existiert nicht: ${dbFile.absolutePath}")
|
Log.e("UPLOAD", "DB fehlt: ${dbFile.absolutePath}")
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
// WAL-Checkpoint
|
// WAL sauber schließen (falls aktiv)
|
||||||
try {
|
try {
|
||||||
val db = SQLiteDatabase.openDatabase(
|
val db = SQLiteDatabase.openDatabase(dbFile.absolutePath, null, SQLiteDatabase.OPEN_READWRITE)
|
||||||
dbFile.absolutePath,
|
db.rawQuery("PRAGMA wal_checkpoint(FULL);", null).use { /* noop */ }
|
||||||
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()
|
db.close()
|
||||||
Log.d("UPLOAD", "WAL-Checkpoint erfolgreich.")
|
} catch (_: Exception) { }
|
||||||
} 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
checkDatabaseExists()
|
||||||
|
uploadPseudoDelta(context, dbFile, token)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("UPLOAD", "Fehler beim Hochladen der DB", e)
|
Log.e("UPLOAD", "Fehler", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkDatabaseExists(): Boolean {
|
private fun checkDatabaseExists(): Boolean {
|
||||||
return try {
|
return try {
|
||||||
val request = Request.Builder()
|
val req = Request.Builder().url(SERVER_CHECK_URL).get().build()
|
||||||
.url(SERVER_CHECK_URL)
|
client.newCall(req).execute().use { resp ->
|
||||||
.get()
|
if (!resp.isSuccessful) return false
|
||||||
.build()
|
val body = resp.body?.string() ?: return false
|
||||||
|
try { JSONObject(body).optBoolean("exists", false) } catch (_: Exception) { false }
|
||||||
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) {
|
} catch (e: Exception) { false }
|
||||||
Log.e("UPLOAD", "Fehler bei Server-Prüfung", e)
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private fun uploadPseudoDelta(context: Context, file: File, token: String) {
|
||||||
* 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 {
|
try {
|
||||||
val db = SQLiteDatabase.openDatabase(file.absolutePath, null, SQLiteDatabase.OPEN_READONLY)
|
val db = SQLiteDatabase.openDatabase(file.absolutePath, null, SQLiteDatabase.OPEN_READONLY)
|
||||||
|
|
||||||
val data = JSONObject().apply {
|
val data = JSONObject().apply {
|
||||||
put("clients", queryToJsonArray(db, "SELECT clientCode FROM clients"))
|
put("clients", queryToJsonArray(db, "SELECT clientCode FROM clients"))
|
||||||
put("questionnaires", queryToJsonArray(db, "SELECT id FROM questionnaires"))
|
put("questionnaires", queryToJsonArray(db, "SELECT id FROM questionnaires"))
|
||||||
@ -116,98 +70,75 @@ object DatabaseUploader {
|
|||||||
put("answers", queryToJsonArray(db, "SELECT clientCode, questionId, answerValue FROM answers"))
|
put("answers", queryToJsonArray(db, "SELECT clientCode, questionId, answerValue FROM answers"))
|
||||||
put(
|
put(
|
||||||
"completed_questionnaires",
|
"completed_questionnaires",
|
||||||
queryToJsonArray(
|
queryToJsonArray(db, "SELECT clientCode, questionnaireId, timestamp, isDone, sumPoints FROM completed_questionnaires")
|
||||||
db,
|
|
||||||
"SELECT clientCode, questionnaireId, timestamp, isDone, sumPoints FROM completed_questionnaires"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
// Schreibe JSON in temporäre Datei
|
// JSON -> verschlüsselte Payload
|
||||||
val tmpJson = File(context.cacheDir, "payload.json")
|
val tmpJson = File(context.cacheDir, "payload.json").apply { writeText(data.toString()) }
|
||||||
tmpJson.writeText(data.toString())
|
|
||||||
|
|
||||||
// Verschlüssele JSON -> tmpEnc
|
|
||||||
val tmpEnc = File(context.cacheDir, "payload.enc")
|
val tmpEnc = File(context.cacheDir, "payload.enc")
|
||||||
try {
|
try {
|
||||||
AES256Helper.encryptFile(tmpJson, tmpEnc)
|
AES256Helper.encryptFileWithToken(tmpJson, tmpEnc, token)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("UPLOAD", "Fehler bei der Verschlüsselung der JSON-Datei", e)
|
Log.e("UPLOAD", "Verschlüsselung fehlgeschlagen", e)
|
||||||
// cleanup
|
tmpJson.delete(); return
|
||||||
tmpJson.delete()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val requestBody = MultipartBody.Builder()
|
val body = MultipartBody.Builder()
|
||||||
.setType(MultipartBody.FORM)
|
.setType(MultipartBody.FORM)
|
||||||
.addFormDataPart("token", API_TOKEN)
|
.addFormDataPart("token", token) // bleibt für Kompatibilität enthalten
|
||||||
// Datei-Feld "file" mit verschlüsselter Payload
|
.addFormDataPart("file", "payload.enc", tmpEnc.asRequestBody("application/octet-stream".toMediaType()))
|
||||||
.addFormDataPart(
|
|
||||||
"file",
|
|
||||||
"payload.enc",
|
|
||||||
tmpEnc.asRequestBody("application/octet-stream".toMediaType())
|
|
||||||
)
|
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
// WICHTIG: Jetzt HTTPS + Konstanten-URL verwenden, plus Bearer-Header
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url(SERVER_DELTA_URL)
|
.url(SERVER_DELTA_URL)
|
||||||
.post(requestBody)
|
.post(body)
|
||||||
|
.header("Authorization", "Bearer $token")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
client.newCall(request).enqueue(object : Callback {
|
client.newCall(request).enqueue(object : Callback {
|
||||||
override fun onFailure(call: Call, e: IOException) {
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
Log.e("UPLOAD", "Delta-Upload fehlgeschlagen: ${e.message}")
|
Log.e("UPLOAD", "Fehlgeschlagen: ${e.message}")
|
||||||
// cleanup
|
tmpJson.delete(); tmpEnc.delete()
|
||||||
tmpJson.delete()
|
|
||||||
tmpEnc.delete()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResponse(call: Call, response: Response) {
|
override fun onResponse(call: Call, response: Response) {
|
||||||
val body = try {
|
val respBody = try { response.body?.string() ?: "" } catch (_: Exception) { "" }
|
||||||
response.body?.string() ?: "Keine Response"
|
|
||||||
} catch (e: Exception) {
|
|
||||||
"Fehler beim Lesen der Response: ${e.message}"
|
|
||||||
}
|
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
Log.d("UPLOAD", "Delta-Upload erfolgreich: $body")
|
Log.d("UPLOAD", "OK: $respBody")
|
||||||
// Lösche Hauptdatenbank
|
|
||||||
if (file.delete()) {
|
// alte Logik: lokale DB + Nebendateien löschen
|
||||||
Log.d("UPLOAD", "Lokale DB gelöscht.")
|
try {
|
||||||
} else {
|
if (!file.delete()) Log.w("UPLOAD", "Lokale DB nicht gelöscht.")
|
||||||
Log.e("UPLOAD", "Löschen der lokalen DB fehlgeschlagen.")
|
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)
|
||||||
}
|
}
|
||||||
// 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 {
|
} else {
|
||||||
Log.e("UPLOAD", "Delta-Upload fehlgeschlagen: ${response.code} $body")
|
Log.e("UPLOAD", "HTTP ${response.code}: $respBody")
|
||||||
tmpJson.delete()
|
|
||||||
tmpEnc.delete()
|
|
||||||
}
|
}
|
||||||
|
tmpJson.delete(); tmpEnc.delete()
|
||||||
|
|
||||||
|
// unverändert beibehalten
|
||||||
|
try { exitProcess(0) } catch (_: Exception) {}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("UPLOAD", "Fehler beim Delta-Upload", e)
|
Log.e("UPLOAD", "Exception", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun queryToJsonArray(db: SQLiteDatabase, query: String): JSONArray {
|
private fun queryToJsonArray(db: SQLiteDatabase, query: String): JSONArray {
|
||||||
val cursor = db.rawQuery(query, null)
|
val c = db.rawQuery(query, null)
|
||||||
val jsonArray = JSONArray()
|
val arr = JSONArray()
|
||||||
cursor.use {
|
c.use {
|
||||||
val columnNames = it.columnNames
|
val cols = it.columnNames
|
||||||
while (it.moveToNext()) {
|
while (it.moveToNext()) {
|
||||||
val obj = JSONObject()
|
val obj = JSONObject()
|
||||||
for (col in columnNames) {
|
for (col in cols) {
|
||||||
val idx = it.getColumnIndex(col)
|
val idx = it.getColumnIndex(col)
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
when (it.getType(idx)) {
|
when (it.getType(idx)) {
|
||||||
@ -215,17 +146,20 @@ object DatabaseUploader {
|
|||||||
Cursor.FIELD_TYPE_FLOAT -> obj.put(col, it.getDouble(idx))
|
Cursor.FIELD_TYPE_FLOAT -> obj.put(col, it.getDouble(idx))
|
||||||
Cursor.FIELD_TYPE_STRING -> obj.put(col, it.getString(idx))
|
Cursor.FIELD_TYPE_STRING -> obj.put(col, it.getString(idx))
|
||||||
Cursor.FIELD_TYPE_NULL -> obj.put(col, JSONObject.NULL)
|
Cursor.FIELD_TYPE_NULL -> obj.put(col, JSONObject.NULL)
|
||||||
Cursor.FIELD_TYPE_BLOB -> {
|
Cursor.FIELD_TYPE_BLOB -> obj.put(col, Base64.encodeToString(it.getBlob(idx), Base64.NO_WRAP))
|
||||||
val blob = it.getBlob(idx)
|
|
||||||
obj.put(col, Base64.encodeToString(blob, Base64.NO_WRAP))
|
|
||||||
}
|
|
||||||
else -> obj.put(col, it.getString(idx))
|
else -> obj.put(col, it.getString(idx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jsonArray.put(obj)
|
arr.put(obj)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return jsonArray
|
return arr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun uploadDatabaseWithToken(context: Context, token: String) {
|
||||||
|
uploadDatabase(context, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
93
app/src/main/java/com/dano/test1/EditButtonHandler.kt
Normal file
93
app/src/main/java/com/dano/test1/EditButtonHandler.kt
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
package com.dano.test1
|
||||||
|
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.Toast
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import com.dano.test1.data.CompletedQuestionnaire
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
178
app/src/main/java/com/dano/test1/ExcelExportService.kt
Normal file
178
app/src/main/java/com/dano/test1/ExcelExportService.kt
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
package com.dano.test1
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.content.Context
|
||||||
|
import android.media.MediaScannerConnection
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Environment
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import org.apache.poi.ss.usermodel.Row
|
||||||
|
import org.apache.poi.xssf.usermodel.XSSFWorkbook
|
||||||
|
|
||||||
|
/*
|
||||||
|
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 = java.io.ByteArrayOutputStream().use { bos ->
|
||||||
|
wb.write(bos); bos.toByteArray()
|
||||||
|
}
|
||||||
|
wb.close()
|
||||||
|
|
||||||
|
return saveToDownloads(
|
||||||
|
filename = "ClientHeaders.xlsx",
|
||||||
|
mimeType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
bytes = bytes
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveToDownloads(filename: String, mimeType: String, bytes: ByteArray): Uri? {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
val resolver = context.contentResolver
|
||||||
|
val values = ContentValues().apply {
|
||||||
|
put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
|
||||||
|
put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
|
||||||
|
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
|
||||||
|
}
|
||||||
|
val collection = MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
||||||
|
val uri = resolver.insert(collection, values)
|
||||||
|
if (uri != null) {
|
||||||
|
resolver.openOutputStream(uri)?.use { it.write(bytes) } ?: return null
|
||||||
|
uri
|
||||||
|
} else null
|
||||||
|
} else {
|
||||||
|
val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||||
|
if (!downloadsDir.exists()) downloadsDir.mkdirs()
|
||||||
|
val outFile = java.io.File(downloadsDir, filename)
|
||||||
|
outFile.writeBytes(bytes)
|
||||||
|
MediaScannerConnection.scanFile(
|
||||||
|
context,
|
||||||
|
arrayOf(outFile.absolutePath),
|
||||||
|
arrayOf(mimeType),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
Uri.fromFile(outFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,11 +2,18 @@ package com.dano.test1
|
|||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
|
import android.util.TypedValue
|
||||||
|
import androidx.core.widget.TextViewCompat
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
/*
|
||||||
|
Zweck :
|
||||||
|
- Steuert die Eingabeseite für „Client Code“ und „Coach Code“ innerhalb des Fragebogen-Flows.
|
||||||
|
*/
|
||||||
|
|
||||||
class HandlerClientCoachCode(
|
class HandlerClientCoachCode(
|
||||||
private val answers: MutableMap<String, Any>,
|
private val answers: MutableMap<String, Any>,
|
||||||
private val languageID: String,
|
private val languageID: String,
|
||||||
@ -24,42 +31,57 @@ class HandlerClientCoachCode(
|
|||||||
this.layout = layout
|
this.layout = layout
|
||||||
this.question = question
|
this.question = question
|
||||||
|
|
||||||
// Bind UI components
|
|
||||||
val clientCodeField = layout.findViewById<EditText>(R.id.client_code)
|
val clientCodeField = layout.findViewById<EditText>(R.id.client_code)
|
||||||
val coachCodeField = layout.findViewById<EditText>(R.id.coach_code)
|
val coachCodeField = layout.findViewById<EditText>(R.id.coach_code)
|
||||||
val questionTextView = layout.findViewById<TextView>(R.id.question)
|
val questionTextView = layout.findViewById<TextView>(R.id.question)
|
||||||
|
val titleTextView = layout.findViewById<TextView>(R.id.textView)
|
||||||
|
|
||||||
// Fill question text using language manager
|
questionTextView.text = question.question?.let { LanguageManager.getText(languageID, it) } ?: ""
|
||||||
questionTextView.text = question.question?.let {
|
|
||||||
LanguageManager.getText(languageID, it)
|
|
||||||
} ?: ""
|
|
||||||
|
|
||||||
// Load last used client code if available
|
setTextSizePercentOfScreenHeight(titleTextView, 0.03f)
|
||||||
val lastClientCode = GlobalValues.LAST_CLIENT_CODE
|
setTextSizePercentOfScreenHeight(questionTextView, 0.03f)
|
||||||
if (!lastClientCode.isNullOrBlank()) {
|
setTextSizePercentOfScreenHeight(clientCodeField, 0.025f)
|
||||||
clientCodeField.setText(lastClientCode)
|
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
|
clientCodeField.isEnabled = false
|
||||||
} else {
|
} else {
|
||||||
clientCodeField.setText(answers["client_code"] as? String ?: "")
|
clientCodeField.setText("")
|
||||||
clientCodeField.isEnabled = true
|
clientCodeField.isEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load saved coach code
|
// Coach-Code immer aus dem Login (TokenStore) setzen und sperren
|
||||||
coachCodeField.setText(answers["coach_code"] as? String ?: "")
|
val coachFromLogin = TokenStore.getUsername(layout.context)
|
||||||
|
if (!coachFromLogin.isNullOrBlank()) {
|
||||||
|
coachCodeField.setText(coachFromLogin)
|
||||||
|
lockCoachField(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
|
||||||
|
}
|
||||||
|
|
||||||
// Set click listener for Next button
|
|
||||||
layout.findViewById<Button>(R.id.Qnext).setOnClickListener {
|
layout.findViewById<Button>(R.id.Qnext).setOnClickListener {
|
||||||
onNextClicked(clientCodeField, coachCodeField)
|
onNextClicked(clientCodeField, coachCodeField)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set click listener for Previous button
|
|
||||||
layout.findViewById<Button>(R.id.Qprev).setOnClickListener {
|
layout.findViewById<Button>(R.id.Qprev).setOnClickListener {
|
||||||
onPreviousClicked(clientCodeField, coachCodeField)
|
onPreviousClicked(clientCodeField, coachCodeField)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle Next button click
|
private fun setTextSizePercentOfScreenHeight(view: TextView, percentOfHeight: Float) {
|
||||||
|
val dm = layout.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)
|
||||||
|
}
|
||||||
|
|
||||||
private fun onNextClicked(clientCodeField: EditText, coachCodeField: EditText) {
|
private fun onNextClicked(clientCodeField: EditText, coachCodeField: EditText) {
|
||||||
|
val loadedClientCode = GlobalValues.LOADED_CLIENT_CODE
|
||||||
|
|
||||||
if (!validate()) {
|
if (!validate()) {
|
||||||
val message = LanguageManager.getText(languageID, "fill_both_fields")
|
val message = LanguageManager.getText(languageID, "fill_both_fields")
|
||||||
showToast(message)
|
showToast(message)
|
||||||
@ -67,62 +89,73 @@ class HandlerClientCoachCode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val clientCode = clientCodeField.text.toString()
|
val clientCode = clientCodeField.text.toString()
|
||||||
val coachCode = coachCodeField.text.toString()
|
// Erzwinge Coach-Code aus Login (falls vorhanden)
|
||||||
|
val coachCode = TokenStore.getUsername(layout.context) ?: coachCodeField.text.toString()
|
||||||
|
|
||||||
// Prüfen, ob die Datenbank-Dateien vor dem Klick existieren
|
// Prüfen, ob die DB-Datei vor dem Zugriff existiert
|
||||||
val dbFile = layout.context.getDatabasePath("questionnaire_database")
|
val dbPath = layout.context.getDatabasePath("questionnaire_database")
|
||||||
val dbJournalFile = layout.context.getDatabasePath("questionnaire_database-journal")
|
val dbExistedBefore = dbPath.exists()
|
||||||
val dbExisted = dbFile.exists() || dbJournalFile.exists()
|
|
||||||
|
|
||||||
// Check if client code already exists asynchronously
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
val existingClient = MyApp.database.clientDao().getClientByCode(clientCode)
|
val existingClient = MyApp.database.clientDao().getClientByCode(clientCode)
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (existingClient != null && clientCodeField.isEnabled) {
|
if (existingClient != null && clientCodeField.isEnabled) {
|
||||||
// Client code already exists and field was editable
|
|
||||||
val message = LanguageManager.getText(languageID, "client_code_exists")
|
val message = LanguageManager.getText(languageID, "client_code_exists")
|
||||||
showToast(message)
|
showToast(message)
|
||||||
} else {
|
} else {
|
||||||
// Either no existing client or re-using previous code
|
|
||||||
saveAnswers(clientCode, coachCode)
|
saveAnswers(clientCode, coachCode)
|
||||||
|
|
||||||
// Datenbank-Dateien löschen, wenn sie vorher NICHT existierten
|
|
||||||
if (!dbExisted) {
|
|
||||||
dbFile.delete()
|
|
||||||
dbJournalFile.delete()
|
|
||||||
}
|
|
||||||
|
|
||||||
goToNextQuestion()
|
goToNextQuestion()
|
||||||
|
|
||||||
|
if (!dbExistedBefore) {
|
||||||
|
MyApp.database.close()
|
||||||
|
dbPath.delete()
|
||||||
|
val journalFile = layout.context.getDatabasePath("questionnaire_database-journal")
|
||||||
|
journalFile.delete()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle Previous button click
|
|
||||||
private fun onPreviousClicked(clientCodeField: EditText, coachCodeField: EditText) {
|
private fun onPreviousClicked(clientCodeField: EditText, coachCodeField: EditText) {
|
||||||
val clientCode = clientCodeField.text.toString()
|
val clientCode = clientCodeField.text.toString()
|
||||||
val coachCode = coachCodeField.text.toString()
|
val coachCode = TokenStore.getUsername(layout.context) ?: coachCodeField.text.toString()
|
||||||
saveAnswers(clientCode, coachCode)
|
saveAnswers(clientCode, coachCode)
|
||||||
goToPreviousQuestion()
|
goToPreviousQuestion()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate that both fields are filled
|
|
||||||
override fun validate(): Boolean {
|
override fun validate(): Boolean {
|
||||||
val clientCode = layout.findViewById<EditText>(R.id.client_code).text
|
val clientCode = layout.findViewById<EditText>(R.id.client_code).text
|
||||||
val coachCode = layout.findViewById<EditText>(R.id.coach_code).text
|
val coachText = layout.findViewById<EditText>(R.id.coach_code).text
|
||||||
return clientCode.isNotBlank() && coachCode.isNotBlank()
|
return clientCode.isNotBlank() && coachText.isNotBlank()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save answers to shared state and global value
|
|
||||||
private fun saveAnswers(clientCode: String, coachCode: String) {
|
private fun saveAnswers(clientCode: String, coachCode: String) {
|
||||||
GlobalValues.LAST_CLIENT_CODE = clientCode
|
GlobalValues.LAST_CLIENT_CODE = clientCode
|
||||||
answers["client_code"] = clientCode
|
answers["client_code"] = clientCode
|
||||||
answers["coach_code"] = coachCode
|
// Speichere garantierten Coach-Code aus Login bevorzugt
|
||||||
|
val loginCoach = TokenStore.getUsername(layout.context)
|
||||||
|
answers["coach_code"] = loginCoach ?: coachCode
|
||||||
}
|
}
|
||||||
|
|
||||||
// Required override but not used here
|
|
||||||
override fun saveAnswer() {
|
override fun saveAnswer() {
|
||||||
// Not used
|
// Not used
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun lockCoachField(field: EditText) {
|
||||||
|
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(8)
|
||||||
|
field.alpha = 0.95f
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun dp(v: Int): Int =
|
||||||
|
(v * layout.resources.displayMetrics.density).toInt()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,12 @@ package com.dano.test1
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
|
|
||||||
|
/*
|
||||||
|
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>,
|
||||||
private val languageID: String,
|
private val languageID: String,
|
||||||
@ -14,7 +20,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 +31,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 +56,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 +66,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()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,9 +2,19 @@ package com.dano.test1
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
|
import kotlinx.coroutines.*
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import android.util.TypedValue
|
||||||
|
import androidx.core.widget.TextViewCompat
|
||||||
|
import android.widget.AbsListView
|
||||||
|
|
||||||
|
/*
|
||||||
|
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 +22,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 +44,22 @@ 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
|
||||||
|
setTextSizePercentOfScreenHeight(textView, 0.03f) // oben
|
||||||
|
setTextSizePercentOfScreenHeight(questionTextView, 0.03f) // frage
|
||||||
|
setTextSizePercentOfScreenHeight(labelDay, 0.025f)
|
||||||
|
setTextSizePercentOfScreenHeight(labelMonth, 0.025f)
|
||||||
|
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,10 +74,54 @@ 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)
|
||||||
|
|
||||||
|
// Spinner responsiv aufsetzen (Schrift + Zeilenhöhe ohne Abschneiden)
|
||||||
setupSpinner(spinnerDay, days, defaultDay)
|
setupSpinner(spinnerDay, days, defaultDay)
|
||||||
setupSpinner(spinnerMonth, months, defaultMonth)
|
setupSpinner(spinnerMonth, months, defaultMonth)
|
||||||
setupSpinner(spinnerYear, years, defaultYear)
|
setupSpinner(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()) {
|
||||||
saveAnswer()
|
saveAnswer()
|
||||||
@ -139,16 +207,71 @@ class HandlerDateSpinner(
|
|||||||
return sdf.parse(dateString)
|
return sdf.parse(dateString)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Textgröße prozentual zur Bildschirmhöhe (in sp)
|
||||||
|
private fun setTextSizePercentOfScreenHeight(view: TextView, percentOfHeight: Float) {
|
||||||
|
val dm = (view.context ?: layout.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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spinner-Adapter: Schrift & Zeilenhöhe dynamisch, kein Abschneiden
|
||||||
private fun <T> setupSpinner(spinner: Spinner, items: List<T>, defaultSelection: T?) {
|
private fun <T> setupSpinner(spinner: Spinner, items: List<T>, defaultSelection: T?) {
|
||||||
val adapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, items)
|
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) // ~2.75% der Bildschirmhöhe
|
||||||
|
val textPx = pxFromSp(textSp)
|
||||||
|
val vPadPx = (textPx * 0.50f).toInt() // vertikales Padding
|
||||||
|
val rowHeight = (textPx * 2.20f + 2 * vPadPx).toInt() // feste Zeilenhöhe
|
||||||
|
|
||||||
|
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 android.view.Gravity.HORIZONTAL_GRAVITY_MASK) or android.view.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)
|
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
spinner.adapter = adapter
|
spinner.adapter = adapter
|
||||||
|
|
||||||
|
spinner.setPadding(spinner.paddingLeft, vPadPx, spinner.paddingRight, vPadPx)
|
||||||
|
spinner.minimumHeight = rowHeight
|
||||||
|
spinner.requestLayout()
|
||||||
|
|
||||||
defaultSelection?.let {
|
defaultSelection?.let {
|
||||||
val index = items.indexOf(it)
|
val index = items.indexOf(it)
|
||||||
if (index >= 0) {
|
if (index >= 0) spinner.setSelection(index)
|
||||||
spinner.setSelection(index)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,20 @@
|
|||||||
package com.dano.test1
|
package com.dano.test1
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.util.TypedValue
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
|
import androidx.core.widget.TextViewCompat
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Zweck:
|
||||||
|
- Stellt eine „Glas-Skala“-Frage dar, bei der pro Symptom (Zeile) genau eine von fünf Antwortstufen gewählt wird: never / little / moderate / much / extreme.
|
||||||
|
- Die Stufen werden sowohl als RadioButtons als auch über eine feste Icon-Leiste visualisiert.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
class HandlerGlassScaleQuestion(
|
class HandlerGlassScaleQuestion(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
@ -12,14 +23,19 @@ class HandlerGlassScaleQuestion(
|
|||||||
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
|
||||||
private lateinit var question: QuestionItem.GlassScaleQuestion
|
private lateinit var question: QuestionItem.GlassScaleQuestion
|
||||||
|
|
||||||
private val scaleLabels = listOf(
|
private val scaleLabels = listOf(
|
||||||
"never_glass", "little_glass", "moderate_glass", "much_glass", "extreme_glass"
|
"never_glass",
|
||||||
|
"little_glass",
|
||||||
|
"moderate_glass",
|
||||||
|
"much_glass",
|
||||||
|
"extreme_glass"
|
||||||
)
|
)
|
||||||
|
|
||||||
private val pointsMap = mapOf(
|
private val pointsMap = mapOf(
|
||||||
@ -30,60 +46,98 @@ class HandlerGlassScaleQuestion(
|
|||||||
"extreme_glass" to 4
|
"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
|
||||||
|
)
|
||||||
|
|
||||||
override fun bind(layout: View, question: QuestionItem) {
|
override fun bind(layout: View, question: QuestionItem) {
|
||||||
if (question !is QuestionItem.GlassScaleQuestion) return
|
if (question !is QuestionItem.GlassScaleQuestion) return
|
||||||
|
|
||||||
this.layout = layout
|
this.layout = layout
|
||||||
this.question = question
|
this.question = question
|
||||||
|
|
||||||
layout.findViewById<TextView>(R.id.textView).text =
|
val titleTv = layout.findViewById<TextView>(R.id.textView)
|
||||||
question.textKey?.let { LanguageManager.getText(languageID, it) } ?: ""
|
val questionTv = layout.findViewById<TextView>(R.id.question)
|
||||||
|
|
||||||
layout.findViewById<TextView>(R.id.question).text =
|
titleTv.text = question.textKey?.let { LanguageManager.getText(languageID, it) } ?: ""
|
||||||
question.question?.let { LanguageManager.getText(languageID, it) } ?: ""
|
questionTv.text = question.question?.let { LanguageManager.getText(languageID, it) } ?: ""
|
||||||
|
|
||||||
|
setTextSizePercentOfScreenHeight(titleTv, 0.03f)
|
||||||
|
setTextSizePercentOfScreenHeight(questionTv, 0.03f)
|
||||||
|
|
||||||
|
// feste Icon-Leiste
|
||||||
|
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)
|
val tableLayout = layout.findViewById<TableLayout>(R.id.glass_table)
|
||||||
tableLayout.removeAllViews()
|
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)
|
addSymptomRows(tableLayout)
|
||||||
|
|
||||||
|
// ggf. Antworten aus DB wiederherstellen
|
||||||
|
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) {
|
||||||
|
val table = tableLayout
|
||||||
|
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 < table.childCount) {
|
||||||
|
val row = table.getChildAt(index) as? TableRow ?: continue
|
||||||
|
val radioGroup = row.getChildAt(1) as? RadioGroup ?: continue
|
||||||
|
for (i in 0 until radioGroup.childCount) {
|
||||||
|
val rb = getRadioFromChild(radioGroup.getChildAt(i)) ?: continue
|
||||||
|
if ((rb.tag as? String)?.trim() == dbAnswer) {
|
||||||
|
rb.isChecked = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
answers[symptomKey] = dbAnswer
|
||||||
|
points.add(pointsMap[dbAnswer] ?: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_: Exception) { /* ignore */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
layout.findViewById<Button>(R.id.Qnext).setOnClickListener {
|
layout.findViewById<Button>(R.id.Qnext).setOnClickListener {
|
||||||
if (validate()) {
|
if (validate()) {
|
||||||
saveAnswer()
|
saveAnswer()
|
||||||
goToNextQuestion()
|
goToNextQuestion()
|
||||||
} else {
|
} else {
|
||||||
val message = LanguageManager.getText(languageID, "select_one_answer_per_row")
|
showToast(LanguageManager.getText(languageID, "select_one_answer_per_row"))
|
||||||
showToast(message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
layout.findViewById<Button>(R.id.Qprev).setOnClickListener { goToPreviousQuestion() }
|
||||||
layout.findViewById<Button>(R.id.Qprev).setOnClickListener {
|
|
||||||
goToPreviousQuestion()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addSymptomRows(table: TableLayout) {
|
private fun addSymptomRows(table: TableLayout) {
|
||||||
@ -101,67 +155,91 @@ class HandlerGlassScaleQuestion(
|
|||||||
text = LanguageManager.getText(languageID, symptomKey)
|
text = LanguageManager.getText(languageID, symptomKey)
|
||||||
layoutParams = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT, 4f)
|
layoutParams = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT, 4f)
|
||||||
setPadding(4, 16, 4, 16)
|
setPadding(4, 16, 4, 16)
|
||||||
|
setTextSizePercentOfScreenHeight(this, 0.022f)
|
||||||
}
|
}
|
||||||
row.addView(symptomText)
|
row.addView(symptomText)
|
||||||
|
|
||||||
val radioGroup = RadioGroup(context).apply {
|
val radioGroup = RadioGroup(context).apply {
|
||||||
orientation = RadioGroup.HORIZONTAL
|
orientation = RadioGroup.HORIZONTAL
|
||||||
layoutParams = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT, 5f)
|
layoutParams = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT, 5f)
|
||||||
|
setPadding(0, 0, 0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
scaleLabels.forEach { labelKey ->
|
scaleLabels.forEach { labelKey ->
|
||||||
val radioButton = RadioButton(context).apply {
|
val cell = FrameLayout(context).apply {
|
||||||
|
layoutParams = RadioGroup.LayoutParams(0, RadioGroup.LayoutParams.WRAP_CONTENT, 1f)
|
||||||
|
}
|
||||||
|
val rb = RadioButton(context).apply {
|
||||||
tag = labelKey
|
tag = labelKey
|
||||||
id = View.generateViewId()
|
id = View.generateViewId()
|
||||||
isChecked = savedLabel == labelKey
|
isChecked = savedLabel == labelKey
|
||||||
layoutParams =
|
setPadding(0, 0, 0, 0)
|
||||||
RadioGroup.LayoutParams(0, RadioGroup.LayoutParams.WRAP_CONTENT, 1f)
|
|
||||||
gravity = Gravity.CENTER
|
|
||||||
}
|
}
|
||||||
radioGroup.addView(radioButton)
|
rb.layoutParams = FrameLayout.LayoutParams(
|
||||||
|
FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||||
|
FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||||
|
Gravity.CENTER
|
||||||
|
)
|
||||||
|
cell.addView(rb)
|
||||||
|
radioGroup.addView(cell)
|
||||||
}
|
}
|
||||||
|
|
||||||
row.addView(radioGroup)
|
row.addView(radioGroup)
|
||||||
table.addView(row)
|
table.addView(row)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun validate(): Boolean {
|
override fun validate(): Boolean {
|
||||||
val table = layout.findViewById<TableLayout>(R.id.glass_table)
|
val table = layout.findViewById<TableLayout>(R.id.glass_table)
|
||||||
for (i in 1 until table.childCount) {
|
for (i in 0 until table.childCount) {
|
||||||
val row = table.getChildAt(i) as TableRow
|
val row = table.getChildAt(i) as TableRow
|
||||||
val radioGroup = row.getChildAt(1) as RadioGroup
|
val radioGroup = row.getChildAt(1) as RadioGroup
|
||||||
if (radioGroup.checkedRadioButtonId == -1) {
|
var anyChecked = false
|
||||||
return 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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun saveAnswer() {
|
override fun saveAnswer() {
|
||||||
// Vorherige Punkte dieser Frage entfernen
|
// alte Punkte entfernen
|
||||||
question.symptoms.forEach {
|
question.symptoms.forEach { key ->
|
||||||
val previousLabel = answers[it] as? String
|
val prev = answers[key] as? String
|
||||||
val previousPoint = pointsMap[previousLabel]
|
prev?.let { pointsMap[it] }?.let { points.remove(it) }
|
||||||
if (previousPoint != null) {
|
|
||||||
points.remove(previousPoint)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val table = layout.findViewById<TableLayout>(R.id.glass_table)
|
val table = layout.findViewById<TableLayout>(R.id.glass_table)
|
||||||
for (i in 1 until table.childCount) {
|
for (i in 0 until table.childCount) {
|
||||||
val row = table.getChildAt(i) as TableRow
|
val row = table.getChildAt(i) as TableRow
|
||||||
val symptomKey = question.symptoms[i - 1]
|
val symptomKey = question.symptoms[i]
|
||||||
|
|
||||||
val radioGroup = row.getChildAt(1) as RadioGroup
|
val radioGroup = row.getChildAt(1) as RadioGroup
|
||||||
val checkedId = radioGroup.checkedRadioButtonId
|
for (j in 0 until radioGroup.childCount) {
|
||||||
if (checkedId != -1) {
|
val rb = getRadioFromChild(radioGroup.getChildAt(j)) ?: continue
|
||||||
val radioButton = radioGroup.findViewById<RadioButton>(checkedId)
|
if (rb.isChecked) {
|
||||||
val selectedLabel = radioButton.tag as String
|
val selected = rb.tag as String
|
||||||
answers[symptomKey] = selectedLabel
|
answers[symptomKey] = selected
|
||||||
|
points.add(pointsMap[selected] ?: 0)
|
||||||
val point = pointsMap[selectedLabel] ?: 0
|
break
|
||||||
points.add(point)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getRadioFromChild(child: View): RadioButton? =
|
||||||
|
when (child) {
|
||||||
|
is RadioButton -> child
|
||||||
|
is FrameLayout -> child.getChildAt(0) as? RadioButton
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setTextSizePercentOfScreenHeight(view: TextView, percentOfHeight: Float) {
|
||||||
|
val dm = (view.context ?: layout.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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,25 @@ import android.view.View
|
|||||||
import android.widget.*
|
import android.widget.*
|
||||||
import android.text.Html
|
import android.text.Html
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.widget.TextViewCompat
|
||||||
|
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(
|
class HandlerLastPage(
|
||||||
private val answers: Map<String, Any>,
|
private val answers: Map<String, Any>,
|
||||||
@ -15,72 +34,113 @@ class HandlerLastPage(
|
|||||||
|
|
||||||
private lateinit var currentQuestion: QuestionItem.LastPage
|
private lateinit var currentQuestion: QuestionItem.LastPage
|
||||||
private lateinit var layout: View
|
private lateinit var layout: View
|
||||||
private val minLoadingTimeMs = 2000L // Minimum loading time in milliseconds (2 seconds)
|
private val minLoadingTimeMs = 2000L
|
||||||
|
|
||||||
override fun bind(layout: View, question: QuestionItem) {
|
override fun bind(layout: View, question: QuestionItem) {
|
||||||
this.layout = layout
|
this.layout = layout
|
||||||
currentQuestion = question as QuestionItem.LastPage
|
currentQuestion = question as QuestionItem.LastPage
|
||||||
|
|
||||||
// Set localized text for the last page
|
val titleTv = layout.findViewById<TextView>(R.id.textView)
|
||||||
layout.findViewById<TextView>(R.id.textView).text =
|
val questionTv = layout.findViewById<TextView>(R.id.question)
|
||||||
LanguageManager.getText(languageID, currentQuestion.textKey)
|
val prevBtn = layout.findViewById<MaterialButton>(R.id.Qprev)
|
||||||
|
val finishBtn = layout.findViewById<MaterialButton>(R.id.Qfinish)
|
||||||
|
|
||||||
// Set question text with HTML formatting
|
// Texte setzen
|
||||||
layout.findViewById<TextView>(R.id.question).text =
|
titleTv.text = LanguageManager.getText(languageID, currentQuestion.textKey)
|
||||||
Html.fromHtml(
|
questionTv.text = Html.fromHtml(
|
||||||
LanguageManager.getText(languageID, currentQuestion.question),
|
LanguageManager.getText(languageID, currentQuestion.question),
|
||||||
Html.FROM_HTML_MODE_LEGACY
|
Html.FROM_HTML_MODE_LEGACY
|
||||||
)
|
)
|
||||||
|
|
||||||
// Setup previous button
|
// Finish-Button: Text + responsive Schrift
|
||||||
layout.findViewById<Button>(R.id.Qprev).setOnClickListener {
|
finishBtn.text = LanguageManager.getText(languageID, "save")
|
||||||
goToPreviousQuestion()
|
finishBtn.isAllCaps = false
|
||||||
}
|
applyResponsiveTextSizing(finishBtn)
|
||||||
|
|
||||||
// Setup finish button
|
// Überschriften responsiv skalieren (wie zuvor)
|
||||||
layout.findViewById<Button>(R.id.Qfinish).setOnClickListener {
|
setTextSizePercentOfScreenHeight(titleTv, 0.03f)
|
||||||
showLoading(true) // Show loading indicator
|
setTextSizePercentOfScreenHeight(questionTv, 0.03f)
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
prevBtn.setOnClickListener { goToPreviousQuestion() }
|
||||||
|
|
||||||
|
finishBtn.setOnClickListener {
|
||||||
|
showLoading(true)
|
||||||
|
|
||||||
// Save answers on a background thread
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
val startTime = System.currentTimeMillis()
|
val startTime = System.currentTimeMillis()
|
||||||
|
|
||||||
// Save answers to database (suspend function)
|
// Antworten speichern
|
||||||
saveAnswersToDatabase(answers)
|
saveAnswersToDatabase(answers)
|
||||||
|
|
||||||
// Calculate total points and update global value
|
// Punkte summieren
|
||||||
GlobalValues.INTEGRATION_INDEX = sumPoints()
|
GlobalValues.INTEGRATION_INDEX = sumPoints()
|
||||||
|
|
||||||
// Save last client code globally if available
|
// Client-Code merken (für Auto-Laden im Opening Screen)
|
||||||
val clientCode = answers["client_code"] as? String
|
val clientCode = answers["client_code"] as? String
|
||||||
if (clientCode != null) GlobalValues.LAST_CLIENT_CODE = clientCode
|
if (clientCode != null) {
|
||||||
|
GlobalValues.LAST_CLIENT_CODE = clientCode
|
||||||
// Ensure loading animation runs at least 2 seconds
|
GlobalValues.LOADED_CLIENT_CODE = clientCode // <— zusätzlich setzen
|
||||||
val elapsedTime = System.currentTimeMillis() - startTime
|
|
||||||
if (elapsedTime < minLoadingTimeMs) {
|
|
||||||
delay(minLoadingTimeMs - elapsedTime)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Switch back to main thread to update UI
|
// min. Ladezeit einhalten (ruhiges UX)
|
||||||
|
val elapsedTime = System.currentTimeMillis() - startTime
|
||||||
|
if (elapsedTime < minLoadingTimeMs) delay(minLoadingTimeMs - elapsedTime)
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
showLoading(false) // Hide loading indicator
|
showLoading(false)
|
||||||
val activity = layout.context as? MainActivity
|
val activity = layout.context as? MainActivity
|
||||||
|
// Zurück zum Opening Screen – der lädt dann automatisch (siehe Änderung 2)
|
||||||
activity?.finishQuestionnaire() ?: goToNextQuestion()
|
activity?.finishQuestionnaire() ?: goToNextQuestion()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun validate(): Boolean = true // No validation needed on last page
|
override fun validate(): Boolean = true
|
||||||
override fun saveAnswer() {} // No answers to save here
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
|
// Helper: Textgröße prozentual zur Bildschirmhöhe setzen (in sp)
|
||||||
|
private fun setTextSizePercentOfScreenHeight(view: TextView, percentOfHeight: Float) {
|
||||||
|
val dm = (view.context ?: layout.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)
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate the sum of all keys ending with "_points"
|
|
||||||
private fun sumPoints(): Int =
|
private fun sumPoints(): Int =
|
||||||
answers.filterKeys { it.endsWith("_points") }
|
answers.filterKeys { it.endsWith("_points") }
|
||||||
.values.mapNotNull { it as? Int }
|
.values.mapNotNull { it as? Int }
|
||||||
.sum()
|
.sum()
|
||||||
|
|
||||||
// Show or hide a ProgressBar (loading spinner)
|
|
||||||
private fun showLoading(show: Boolean) {
|
private fun showLoading(show: Boolean) {
|
||||||
val progressBar = layout.findViewById<ProgressBar>(R.id.progressBar)
|
val progressBar = layout.findViewById<ProgressBar>(R.id.progressBar)
|
||||||
val finishButton = layout.findViewById<Button>(R.id.Qfinish)
|
val finishButton = layout.findViewById<Button>(R.id.Qfinish)
|
||||||
|
|||||||
@ -3,6 +3,14 @@ package com.dano.test1
|
|||||||
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 android.util.TypedValue
|
||||||
|
import androidx.core.widget.TextViewCompat
|
||||||
|
|
||||||
|
/*
|
||||||
|
Zweck:
|
||||||
|
- Steuert eine Frage mit mehreren auswählbaren Antwortoptionen (Checkboxen).
|
||||||
|
*/
|
||||||
|
|
||||||
class HandlerMultiCheckboxQuestion(
|
class HandlerMultiCheckboxQuestion(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
@ -11,7 +19,8 @@ class HandlerMultiCheckboxQuestion(
|
|||||||
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
|
||||||
@ -25,21 +34,40 @@ class HandlerMultiCheckboxQuestion(
|
|||||||
val questionTitle = layout.findViewById<TextView>(R.id.question)
|
val questionTitle = layout.findViewById<TextView>(R.id.question)
|
||||||
val questionTextView = layout.findViewById<TextView>(R.id.textView)
|
val questionTextView = layout.findViewById<TextView>(R.id.textView)
|
||||||
|
|
||||||
// Hier jetzt identisch zur RadioQuestion:
|
|
||||||
questionTextView.text = this.question.textKey?.let { LanguageManager.getText(languageID, it) } ?: ""
|
questionTextView.text = this.question.textKey?.let { LanguageManager.getText(languageID, it) } ?: ""
|
||||||
questionTitle.text = this.question.question?.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)
|
||||||
|
setTextSizePercentOfScreenHeight(questionTextView, 0.03f) // Überschrift
|
||||||
|
setTextSizePercentOfScreenHeight(questionTitle, 0.03f) // Frage
|
||||||
|
|
||||||
container.removeAllViews()
|
container.removeAllViews()
|
||||||
|
|
||||||
|
// bestehende Auswahl aus answers (falls vorhanden) als Set
|
||||||
val selectedKeys = this.question.question?.let {
|
val selectedKeys = this.question.question?.let {
|
||||||
(answers[it] as? List<*>)?.map { it.toString() }?.toSet()
|
(answers[it] as? List<*>)?.map { it.toString() }?.toSet()
|
||||||
} ?: emptySet()
|
} ?: 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 ->
|
this.question.options.forEach { option ->
|
||||||
val checkBox = CheckBox(context).apply {
|
val checkBox = CheckBox(context).apply {
|
||||||
text = LanguageManager.getText(languageID, option.key)
|
text = LanguageManager.getText(languageID, option.key)
|
||||||
tag = option.key
|
tag = option.key
|
||||||
isChecked = selectedKeys.contains(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(
|
layoutParams = LinearLayout.LayoutParams(
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||||
@ -52,6 +80,44 @@ class HandlerMultiCheckboxQuestion(
|
|||||||
container.addView(checkBox)
|
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 {
|
layout.findViewById<Button>(R.id.Qnext).setOnClickListener {
|
||||||
if (validate()) {
|
if (validate()) {
|
||||||
saveAnswer()
|
saveAnswer()
|
||||||
@ -97,8 +163,51 @@ class HandlerMultiCheckboxQuestion(
|
|||||||
}
|
}
|
||||||
|
|
||||||
question.question?.let { questionKey ->
|
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
|
answers[questionKey] = selectedKeys
|
||||||
points.add(totalPoints)
|
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('"', '\''))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setTextSizePercentOfScreenHeight(view: TextView, percentOfHeight: Float) {
|
||||||
|
val dm = (view.context ?: layout.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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -4,6 +4,14 @@ 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 android.util.TypedValue
|
||||||
|
import androidx.core.widget.TextViewCompat // <— hinzugefügt
|
||||||
|
|
||||||
|
/*
|
||||||
|
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 +21,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 +41,11 @@ class HandlerRadioQuestion(
|
|||||||
Html.fromHtml(LanguageManager.getText(languageID, it), Html.FROM_HTML_MODE_LEGACY)
|
Html.fromHtml(LanguageManager.getText(languageID, it), Html.FROM_HTML_MODE_LEGACY)
|
||||||
} ?: ""
|
} ?: ""
|
||||||
|
|
||||||
|
//
|
||||||
|
// Titel/Frage: 3% der Bildschirmhöhe
|
||||||
|
setTextSizePercentOfScreenHeight(questionTextView, 0.03f)
|
||||||
|
setTextSizePercentOfScreenHeight(questionTitle, 0.03f)
|
||||||
|
// ===================================================
|
||||||
|
|
||||||
radioGroup.removeAllViews()
|
radioGroup.removeAllViews()
|
||||||
|
|
||||||
@ -39,6 +53,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
|
||||||
|
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 +65,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()
|
||||||
@ -78,6 +137,15 @@ class HandlerRadioQuestion(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setzt Textgröße prozentual zur Bildschirmhöhe (in sp)
|
||||||
|
private fun setTextSizePercentOfScreenHeight(view: TextView, percentOfHeight: Float) {
|
||||||
|
val dm = (view.context ?: layout.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)
|
||||||
|
}
|
||||||
|
// ————————————————————————————————————————————————————————————————
|
||||||
|
|
||||||
private fun restorePreviousAnswer(radioGroup: RadioGroup) {
|
private fun restorePreviousAnswer(radioGroup: RadioGroup) {
|
||||||
question.question?.let { questionKey ->
|
question.question?.let { questionKey ->
|
||||||
val savedAnswer = answers[questionKey] as? String
|
val savedAnswer = answers[questionKey] as? String
|
||||||
@ -104,10 +172,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 +182,4 @@ class HandlerRadioQuestion(
|
|||||||
points.add(newPoint)
|
points.add(newPoint)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,18 @@ package com.dano.test1
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.widget.TextViewCompat
|
||||||
|
|
||||||
|
/*
|
||||||
|
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 +21,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,15 +38,47 @@ 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 (wie im HandlerRadioQuestion)
|
||||||
|
setTextSizePercentOfScreenHeight(textView, 0.03f)
|
||||||
|
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 }
|
||||||
|
|
||||||
|
// Spinner aufsetzen
|
||||||
setupSpinner(spinner, options, savedSelection)
|
setupSpinner(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()) {
|
||||||
saveAnswer()
|
saveAnswer()
|
||||||
@ -63,10 +107,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 +119,73 @@ class HandlerStringSpinner(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Textgröße prozentual zur Bildschirmhöhe setzen und AutoSize deaktivieren
|
||||||
|
private fun setTextSizePercentOfScreenHeight(view: TextView, percentOfHeight: Float) {
|
||||||
|
val dm = (view.context ?: layout.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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spinner-Adapter mit dynamischer Schrift & stabiler Dropdown-Zeilenhöhe (kein Abschneiden)
|
||||||
private fun <T> setupSpinner(spinner: Spinner, items: List<T>, selectedItem: T?) {
|
private fun <T> setupSpinner(spinner: Spinner, items: List<T>, selectedItem: T?) {
|
||||||
val adapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, items)
|
val dm = context.resources.displayMetrics
|
||||||
|
|
||||||
|
fun spFromScreenHeight(percent: Float): Float =
|
||||||
|
(dm.heightPixels * percent) / dm.scaledDensity
|
||||||
|
fun pxFromSp(sp: Float): Int = (sp * dm.scaledDensity).toInt()
|
||||||
|
|
||||||
|
// Schrift & abgeleitete Höhen (wie beim Value-Spinner-Fix)
|
||||||
|
val textSp = spFromScreenHeight(0.0275f) // ~2.75% der Bildschirmhöhe
|
||||||
|
val textPx = pxFromSp(textSp)
|
||||||
|
val vPadPx = (textPx * 0.50f).toInt() // vertikales Padding
|
||||||
|
val rowHeight = (textPx * 2.20f + 2 * vPadPx).toInt() // feste Zeilenhöhe, verhindert Abschneiden
|
||||||
|
|
||||||
|
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 android.view.Gravity.HORIZONTAL_GRAVITY_MASK) or android.view.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) // ausgewählte Ansicht
|
||||||
|
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) // Dropdown-Zeilen: Höhe erzwingen
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
spinner.adapter = adapter
|
spinner.adapter = adapter
|
||||||
|
|
||||||
|
// Spinner selbst ausreichend hoch machen
|
||||||
|
spinner.setPadding(spinner.paddingLeft, vPadPx, spinner.paddingRight, vPadPx)
|
||||||
|
spinner.minimumHeight = rowHeight
|
||||||
|
spinner.requestLayout()
|
||||||
|
|
||||||
selectedItem?.let {
|
selectedItem?.let {
|
||||||
val index = items.indexOf(it)
|
val index = items.indexOf(it)
|
||||||
if (index >= 0) {
|
if (index >= 0) spinner.setSelection(index)
|
||||||
spinner.setSelection(index)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,19 @@ package com.dano.test1
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import android.util.TypedValue
|
||||||
|
import androidx.core.widget.TextViewCompat // <- NEU
|
||||||
|
|
||||||
|
/*
|
||||||
|
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 +23,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 +43,11 @@ 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) } ?: ""
|
||||||
|
|
||||||
|
// Schriftgrößen wie im HandlerRadioQuestion
|
||||||
|
// Titel/Frage: 3% der Bildschirmhöhe
|
||||||
|
setTextSizePercentOfScreenHeight(textView, 0.03f)
|
||||||
|
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() }
|
||||||
@ -40,6 +58,31 @@ class HandlerValueSpinner(
|
|||||||
val savedValue = question.question?.let { answers[it] as? String }
|
val savedValue = question.question?.let { answers[it] as? String }
|
||||||
setupSpinner(spinner, spinnerItems, savedValue)
|
setupSpinner(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()) {
|
||||||
saveAnswer()
|
saveAnswer()
|
||||||
@ -84,14 +127,72 @@ class HandlerValueSpinner(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setzt Textgröße prozentual zur Bildschirmhöhe (in sp)
|
||||||
|
private fun setTextSizePercentOfScreenHeight(view: TextView, percentOfHeight: Float) {
|
||||||
|
val dm = (view.context ?: layout.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)
|
||||||
|
}
|
||||||
|
|
||||||
private fun <T> setupSpinner(spinner: Spinner, items: List<T>, selectedItem: T?) {
|
private fun <T> setupSpinner(spinner: Spinner, items: List<T>, selectedItem: T?) {
|
||||||
val adapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, items)
|
val dm = context.resources.displayMetrics
|
||||||
|
|
||||||
|
fun spFromScreenHeight(percent: Float): Float =
|
||||||
|
(dm.heightPixels * percent) / dm.scaledDensity
|
||||||
|
fun pxFromSp(sp: Float): Int = (sp * dm.scaledDensity).toInt()
|
||||||
|
|
||||||
|
// Schrift & abgeleitete Höhen
|
||||||
|
val textSp = spFromScreenHeight(0.0275f) // ~2.75% der Bildschirmhöhe
|
||||||
|
val textPx = pxFromSp(textSp)
|
||||||
|
val vPadPx = (textPx * 0.50f).toInt() // vertikales Padding
|
||||||
|
val rowHeight = (textPx * 2.20f + 2 * vPadPx).toInt() // feste Zeilenhöhe
|
||||||
|
|
||||||
|
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 android.view.Gravity.HORIZONTAL_GRAVITY_MASK) or android.view.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) // ausgewählte Ansicht
|
||||||
|
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) // Dropdown-Zeilen: Höhe erzwingen
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
spinner.adapter = adapter
|
spinner.adapter = adapter
|
||||||
|
|
||||||
|
// Spinner selbst ausreichend hoch machen
|
||||||
|
spinner.setPadding(spinner.paddingLeft, vPadPx, spinner.paddingRight, vPadPx)
|
||||||
|
spinner.minimumHeight = rowHeight
|
||||||
|
spinner.requestLayout()
|
||||||
|
|
||||||
selectedItem?.let {
|
selectedItem?.let {
|
||||||
val index = items.indexOf(it)
|
val index = items.indexOf(it)
|
||||||
if (index >= 0) spinner.setSelection(index)
|
if (index >= 0) spinner.setSelection(index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
87
app/src/main/java/com/dano/test1/HeaderOrderRepository.kt
Normal file
87
app/src/main/java/com/dano/test1/HeaderOrderRepository.kt
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
package com.dano.test1
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
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) {
|
||||||
|
org.apache.poi.ss.usermodel.CellType.STRING -> cell.stringCellValue
|
||||||
|
org.apache.poi.ss.usermodel.CellType.NUMERIC ->
|
||||||
|
if (org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(cell))
|
||||||
|
cell.dateCellValue.time.toString()
|
||||||
|
else {
|
||||||
|
val n = cell.numericCellValue
|
||||||
|
if (n % 1.0 == 0.0) n.toLong().toString() else n.toString()
|
||||||
|
}
|
||||||
|
org.apache.poi.ss.usermodel.CellType.BOOLEAN -> cell.booleanCellValue.toString()
|
||||||
|
org.apache.poi.ss.usermodel.CellType.FORMULA -> cell.richStringCellValue.string
|
||||||
|
else -> ""
|
||||||
|
}.trim()
|
||||||
|
|
||||||
|
if (value.isEmpty()) continue
|
||||||
|
if (i == first && value == "#") continue // „#“ in Spalte 0 ignorieren
|
||||||
|
out.add(value)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
158
app/src/main/java/com/dano/test1/LoadButtonHandler.kt
Normal file
158
app/src/main/java/com/dano/test1/LoadButtonHandler.kt
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
package com.dano.test1
|
||||||
|
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.Toast
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import com.dano.test1.data.CompletedQuestionnaire
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
218
app/src/main/java/com/dano/test1/LoginManager.kt
Normal file
218
app/src/main/java/com/dano/test1/LoginManager.kt
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
package com.dano.test1
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
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 = android.text.InputType.TYPE_CLASS_TEXT or
|
||||||
|
android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||||
|
}
|
||||||
|
val etRepeat = EditText(context).apply {
|
||||||
|
hint = "Neues Passwort (wiederholen)"
|
||||||
|
inputType = android.text.InputType.TYPE_CLASS_TEXT or
|
||||||
|
android.text.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}") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,19 @@
|
|||||||
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 java.io.File
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
@ -10,18 +22,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
|
|
||||||
openingScreenHandler = HandlerOpeningScreen(this)
|
// === Offline-Start ermöglichen ===
|
||||||
openingScreenHandler.init()
|
// 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.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 +212,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
|
||||||
openingScreenHandler.init()
|
if (::openingScreenHandler.isInitialized) {
|
||||||
|
openingScreenHandler.init()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
33
app/src/main/java/com/dano/test1/NetworkUtilis.kt
Normal file
33
app/src/main/java/com/dano/test1/NetworkUtilis.kt
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package com.dano.test1
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -155,14 +155,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 +177,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))
|
||||||
|
|||||||
221
app/src/main/java/com/dano/test1/SaveButtonHandler.kt
Normal file
221
app/src/main/java/com/dano/test1/SaveButtonHandler.kt
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
package com.dano.test1
|
||||||
|
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.pdf.PdfDocument
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.Toast
|
||||||
|
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(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) {
|
||||||
|
val msg = LanguageManager.getText(languageIDProvider(), "saved_pdf_csv")
|
||||||
|
Toast.makeText(activity, msg, 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) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
46
app/src/main/java/com/dano/test1/TokenStore.kt
Normal file
46
app/src/main/java/com/dano/test1/TokenStore.kt
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package com.dano.test1
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
4
app/src/main/res/color/btn_nav_left_icon_tint.xml
Normal file
4
app/src/main/res/color/btn_nav_left_icon_tint.xml
Normal 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>
|
||||||
4
app/src/main/res/color/btn_nav_left_ripple.xml
Normal file
4
app/src/main/res/color/btn_nav_left_ripple.xml
Normal 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>
|
||||||
5
app/src/main/res/color/btn_nav_left_tint.xml
Normal file
5
app/src/main/res/color/btn_nav_left_tint.xml
Normal 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>
|
||||||
4
app/src/main/res/color/btn_nav_right_icon_tint.xml
Normal file
4
app/src/main/res/color/btn_nav_right_icon_tint.xml
Normal 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>
|
||||||
4
app/src/main/res/color/btn_nav_right_ripple.xml
Normal file
4
app/src/main/res/color/btn_nav_right_ripple.xml
Normal 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>
|
||||||
5
app/src/main/res/color/btn_nav_right_tint.xml
Normal file
5
app/src/main/res/color/btn_nav_right_tint.xml
Normal 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>
|
||||||
5
app/src/main/res/drawable/bg_chip_amber.xml
Normal file
5
app/src/main/res/drawable/bg_chip_amber.xml
Normal 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>
|
||||||
5
app/src/main/res/drawable/bg_chip_green.xml
Normal file
5
app/src/main/res/drawable/bg_chip_green.xml
Normal 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>
|
||||||
5
app/src/main/res/drawable/bg_chip_grey.xml
Normal file
5
app/src/main/res/drawable/bg_chip_grey.xml
Normal 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>
|
||||||
7
app/src/main/res/drawable/bg_field_filled.xml
Normal file
7
app/src/main/res/drawable/bg_field_filled.xml
Normal 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>
|
||||||
7
app/src/main/res/drawable/bg_field_locked.xml
Normal file
7
app/src/main/res/drawable/bg_field_locked.xml
Normal 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>
|
||||||
11
app/src/main/res/drawable/ic_chevron_left.xml
Normal file
11
app/src/main/res/drawable/ic_chevron_left.xml
Normal 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>
|
||||||
11
app/src/main/res/drawable/ic_chevron_right.xml
Normal file
11
app/src/main/res/drawable/ic_chevron_right.xml
Normal 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>
|
||||||
8
app/src/main/res/drawable/ic_dot_16.xml
Normal file
8
app/src/main/res/drawable/ic_dot_16.xml
Normal 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>
|
||||||
9
app/src/main/res/drawable/ic_glass_0.xml
Normal file
9
app/src/main/res/drawable/ic_glass_0.xml
Normal 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>
|
||||||
7
app/src/main/res/drawable/ic_glass_1.xml
Normal file
7
app/src/main/res/drawable/ic_glass_1.xml
Normal 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>
|
||||||
7
app/src/main/res/drawable/ic_glass_2.xml
Normal file
7
app/src/main/res/drawable/ic_glass_2.xml
Normal 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>
|
||||||
7
app/src/main/res/drawable/ic_glass_3.xml
Normal file
7
app/src/main/res/drawable/ic_glass_3.xml
Normal 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>
|
||||||
7
app/src/main/res/drawable/ic_glass_4.xml
Normal file
7
app/src/main/res/drawable/ic_glass_4.xml
Normal 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>
|
||||||
8
app/src/main/res/drawable/ic_lock_24.xml
Normal file
8
app/src/main/res/drawable/ic_lock_24.xml
Normal 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>
|
||||||
@ -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>
|
||||||
@ -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"
|
||||||
|
|||||||
87
app/src/main/res/layout/client_overview_screen.xml
Normal file
87
app/src/main/res/layout/client_overview_screen.xml
Normal 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>
|
||||||
69
app/src/main/res/layout/database_screen.xml
Normal file
69
app/src/main/res/layout/database_screen.xml
Normal 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>
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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>
|
||||||
@ -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_height="53dp"
|
|
||||||
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.897"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintVertical_bias="0.091" />
|
|
||||||
|
|
||||||
<Spinner
|
|
||||||
android:id="@+id/string_spinner1"
|
|
||||||
android:layout_width="143dp"
|
|
||||||
android:layout_height="43dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintHorizontal_bias="0.07"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintVertical_bias="0.091" />
|
|
||||||
|
|
||||||
<!-- Container für dynamische Buttons -->
|
|
||||||
|
|
||||||
<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_marginTop="32dp"
|
||||||
android:orientation="vertical"
|
android:paddingStart="24dp"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
android:paddingEnd="24dp"
|
||||||
app:layout_constraintHorizontal_bias="0.0"
|
android:paddingTop="20dp"
|
||||||
|
android:paddingBottom="20dp"
|
||||||
|
app:cardBackgroundColor="@color/brand_surface"
|
||||||
|
app:cardCornerRadius="16dp"
|
||||||
|
app:strokeColor="@color/brand_stroke"
|
||||||
|
app:strokeWidth="1dp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/textView" />
|
app:layout_constraintEnd_toEndOf="parent">
|
||||||
|
|
||||||
<Button
|
<LinearLayout
|
||||||
android:id="@+id/loadButton"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="72dp"
|
android:layout_height="wrap_content"
|
||||||
android:layout_height="52dp"
|
android:orientation="vertical">
|
||||||
app:layout_constraintEnd_toEndOf="@id/editText"
|
|
||||||
app:layout_constraintHorizontal_bias="0.0"
|
|
||||||
app:layout_constraintStart_toStartOf="@id/editText"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/editText" />
|
|
||||||
|
|
||||||
<Button
|
<!-- Language (eigene Zeile) -->
|
||||||
android:id="@+id/uploadButton"
|
<LinearLayout
|
||||||
android:layout_width="200dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="42dp"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="52dp"
|
android:orientation="vertical"
|
||||||
app:layout_constraintEnd_toEndOf="@id/editText"
|
android:paddingBottom="6dp">
|
||||||
app:layout_constraintStart_toStartOf="@id/editText"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/editText" />
|
|
||||||
|
|
||||||
<Button
|
<TextView
|
||||||
android:id="@+id/downloadButton"
|
android:layout_width="wrap_content"
|
||||||
android:layout_width="200dp"
|
android:layout_height="wrap_content"
|
||||||
android:layout_height="42dp"
|
android:paddingStart="12dp"
|
||||||
android:layout_marginTop="92dp"
|
android:paddingBottom="6dp"
|
||||||
app:layout_constraintEnd_toEndOf="@id/editText"
|
android:text="Language"
|
||||||
app:layout_constraintStart_toStartOf="@id/editText"
|
android:textColor="@color/brand_text_dark"
|
||||||
app:layout_constraintTop_toBottomOf="@id/editText" />
|
android:textSize="12sp" />
|
||||||
|
|
||||||
<Button
|
<Spinner
|
||||||
android:id="@+id/editButton"
|
android:id="@+id/string_spinner1"
|
||||||
android:layout_width="72dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="52dp"
|
android:layout_height="48dp"
|
||||||
app:layout_constraintEnd_toEndOf="@id/editText"
|
android:layout_marginStart="8dp"
|
||||||
app:layout_constraintStart_toStartOf="@id/editText"
|
android:layout_marginEnd="8dp"
|
||||||
app:layout_constraintTop_toBottomOf="@id/editText" />
|
android:background="@drawable/bg_field_filled"
|
||||||
|
android:paddingStart="12dp"
|
||||||
|
android:paddingEnd="12dp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<Button
|
<!-- Client Code | Coach Code (gemeinsame Zeile) -->
|
||||||
android:id="@+id/saveButton"
|
<LinearLayout
|
||||||
android:layout_width="72dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="52dp"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintEnd_toEndOf="@id/editText"
|
android:orientation="horizontal"
|
||||||
app:layout_constraintHorizontal_bias="1.0"
|
android:gravity="center_vertical"
|
||||||
app:layout_constraintStart_toStartOf="@id/editText"
|
android:baselineAligned="false"
|
||||||
app:layout_constraintTop_toBottomOf="@id/editText" />
|
android:paddingBottom="6dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginEnd="8dp">
|
||||||
|
|
||||||
|
<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_constraintEnd_toEndOf="parent">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/loadButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="@dimen/pill_height"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
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"/>
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/editButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="@dimen/pill_height"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
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"/>
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/saveButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="@dimen/pill_height"
|
||||||
|
android:layout_weight="1"
|
||||||
|
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"/>
|
||||||
|
</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>
|
||||||
62
app/src/main/res/layout/questionnaire_detail_screen.xml
Normal file
62
app/src/main/res/layout/questionnaire_detail_screen.xml
Normal 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>
|
||||||
@ -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>
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
9
app/src/main/res/values/dimens.xml
Normal file
9
app/src/main/res/values/dimens.xml
Normal 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>
|
||||||
Reference in New Issue
Block a user