new apk and commands added
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,4 +1,3 @@
|
|||||||
// app/src/main/java/com/dano/test1/AES256Helper.kt
|
|
||||||
package com.dano.test1
|
package com.dano.test1
|
||||||
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -11,7 +10,6 @@ import kotlin.math.min
|
|||||||
|
|
||||||
object AES256Helper {
|
object AES256Helper {
|
||||||
|
|
||||||
// HKDF-SHA256: IKM = tokenHex->bytes, salt="", info="qdb-aes", len=32
|
|
||||||
private fun hkdfFromToken(tokenHex: String, info: String = "qdb-aes", len: Int = 32): ByteArray {
|
private fun hkdfFromToken(tokenHex: String, info: String = "qdb-aes", len: Int = 32): ByteArray {
|
||||||
val ikm = hexToBytes(tokenHex)
|
val ikm = hexToBytes(tokenHex)
|
||||||
val mac = Mac.getInstance("HmacSHA256")
|
val mac = Mac.getInstance("HmacSHA256")
|
||||||
|
|||||||
@ -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,9 +30,9 @@ 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)
|
||||||
|
|
||||||
@ -30,11 +40,12 @@ interface QuestionnaireDao {
|
|||||||
suspend fun getById(id: String): Questionnaire?
|
suspend fun getById(id: String): Questionnaire?
|
||||||
|
|
||||||
@Query("SELECT * FROM questionnaires")
|
@Query("SELECT * FROM questionnaires")
|
||||||
suspend fun getAll(): List<Questionnaire> // <-- NEU
|
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>)
|
||||||
|
|
||||||
@ -48,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>)
|
||||||
|
|
||||||
@ -79,9 +92,9 @@ interface AnswerDao {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface CompletedQuestionnaireDao {
|
interface CompletedQuestionnaireDao {
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insert(entry: CompletedQuestionnaire)
|
suspend fun insert(entry: CompletedQuestionnaire)
|
||||||
|
|
||||||
@ -93,4 +106,4 @@ interface CompletedQuestionnaireDao {
|
|||||||
|
|
||||||
@Query("SELECT questionnaireId FROM completed_questionnaires WHERE clientCode = :clientCode")
|
@Query("SELECT questionnaireId FROM completed_questionnaires WHERE clientCode = :clientCode")
|
||||||
suspend fun getCompletedQuestionnairesForClient(clientCode: String): List<String>
|
suspend fun getCompletedQuestionnairesForClient(clientCode: String): List<String>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -10,12 +10,29 @@ import android.provider.MediaStore
|
|||||||
import org.apache.poi.ss.usermodel.Row
|
import org.apache.poi.ss.usermodel.Row
|
||||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook
|
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(
|
class ExcelExportService(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val headerRepo: HeaderOrderRepository
|
private val headerRepo: HeaderOrderRepository
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/** Baut die Excel-Datei und speichert sie ausschließlich unter "Downloads". */
|
/* Baut die Excel-Datei und speichert sie ausschließlich unter "Downloads". */
|
||||||
suspend fun exportHeadersForAllClients(): Uri? {
|
suspend fun exportHeadersForAllClients(): Uri? {
|
||||||
val orderedIds = headerRepo.loadOrderedIds()
|
val orderedIds = headerRepo.loadOrderedIds()
|
||||||
if (orderedIds.isEmpty()) return null
|
if (orderedIds.isEmpty()) return null
|
||||||
@ -78,7 +95,6 @@ class ExcelExportService(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Speichert Bytes nach "Downloads". */
|
|
||||||
private fun saveToDownloads(filename: String, mimeType: String, bytes: ByteArray): Uri? {
|
private fun saveToDownloads(filename: String, mimeType: String, bytes: ByteArray): Uri? {
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
val resolver = context.contentResolver
|
val resolver = context.contentResolver
|
||||||
@ -108,7 +124,6 @@ class ExcelExportService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Export-spezifische Lokalisierung (EN) ----------
|
|
||||||
private suspend fun englishQuestionForId(id: String, questionnaireIdSet: Set<String>): String {
|
private suspend fun englishQuestionForId(id: String, questionnaireIdSet: Set<String>): String {
|
||||||
if (id == "client_code") return "Client code"
|
if (id == "client_code") return "Client code"
|
||||||
if (id in questionnaireIdSet && !id.contains('-')) return "Questionnaire status"
|
if (id in questionnaireIdSet && !id.contains('-')) return "Questionnaire status"
|
||||||
@ -147,7 +162,7 @@ class ExcelExportService(
|
|||||||
return stripped
|
return stripped
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Englisch für Export; belässt Done/Not Done/None. */
|
/* Englisch für Export; belässt Done/Not Done/None. */
|
||||||
private fun localizeForExportEn(id: String, raw: String): String {
|
private fun localizeForExportEn(id: String, raw: String): String {
|
||||||
if (id == "client_code") return raw
|
if (id == "client_code") return raw
|
||||||
if (raw == "Done" || raw == "Not Done" || raw == "None") return raw
|
if (raw == "Done" || raw == "Not Done" || raw == "None") return raw
|
||||||
|
|||||||
@ -9,6 +9,11 @@ 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,
|
||||||
@ -48,7 +53,7 @@ class HandlerClientCoachCode(
|
|||||||
clientCodeField.isEnabled = true
|
clientCodeField.isEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// === NEU: Coach-Code immer aus dem Login (TokenStore) setzen und sperren ===
|
// Coach-Code immer aus dem Login (TokenStore) setzen und sperren
|
||||||
val coachFromLogin = TokenStore.getUsername(layout.context)
|
val coachFromLogin = TokenStore.getUsername(layout.context)
|
||||||
if (!coachFromLogin.isNullOrBlank()) {
|
if (!coachFromLogin.isNullOrBlank()) {
|
||||||
coachCodeField.setText(coachFromLogin)
|
coachCodeField.setText(coachFromLogin)
|
||||||
@ -122,7 +127,6 @@ class HandlerClientCoachCode(
|
|||||||
|
|
||||||
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
|
||||||
// Validierung nimmt den (ggf. gesperrten) Text – passt
|
|
||||||
val coachText = layout.findViewById<EditText>(R.id.coach_code).text
|
val coachText = layout.findViewById<EditText>(R.id.coach_code).text
|
||||||
return clientCode.isNotBlank() && coachText.isNotBlank()
|
return clientCode.isNotBlank() && coachText.isNotBlank()
|
||||||
}
|
}
|
||||||
@ -139,7 +143,6 @@ class HandlerClientCoachCode(
|
|||||||
// Not used
|
// Not used
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Helfer zum Sperren inkl. optischer Markierung (wie im Opening Screen) ---
|
|
||||||
private fun lockCoachField(field: EditText) {
|
private fun lockCoachField(field: EditText) {
|
||||||
field.isFocusable = false
|
field.isFocusable = false
|
||||||
field.isFocusableInTouchMode = false
|
field.isFocusableInTouchMode = false
|
||||||
|
|||||||
@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,11 @@ import android.util.TypedValue
|
|||||||
import androidx.core.widget.TextViewCompat
|
import androidx.core.widget.TextViewCompat
|
||||||
import android.widget.AbsListView
|
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,
|
||||||
private val answers: MutableMap<String, Any>,
|
private val answers: MutableMap<String, Any>,
|
||||||
@ -46,13 +51,13 @@ class HandlerDateSpinner(
|
|||||||
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 ——
|
// Schriftgrößen pro Bildschirmhöhe
|
||||||
setTextSizePercentOfScreenHeight(textView, 0.03f) // oben
|
setTextSizePercentOfScreenHeight(textView, 0.03f) // oben
|
||||||
setTextSizePercentOfScreenHeight(questionTextView, 0.03f) // frage
|
setTextSizePercentOfScreenHeight(questionTextView, 0.03f) // frage
|
||||||
setTextSizePercentOfScreenHeight(labelDay, 0.025f)
|
setTextSizePercentOfScreenHeight(labelDay, 0.025f)
|
||||||
setTextSizePercentOfScreenHeight(labelMonth, 0.025f)
|
setTextSizePercentOfScreenHeight(labelMonth, 0.025f)
|
||||||
setTextSizePercentOfScreenHeight(labelYear, 0.025f)
|
setTextSizePercentOfScreenHeight(labelYear, 0.025f)
|
||||||
// ————————————————————————————————
|
//
|
||||||
|
|
||||||
// gespeicherte Antwort (YYYY-MM-DD) lesen
|
// gespeicherte Antwort (YYYY-MM-DD) lesen
|
||||||
val (savedYear, savedMonthIndex, savedDay) = question.question?.let {
|
val (savedYear, savedMonthIndex, savedDay) = question.question?.let {
|
||||||
@ -202,7 +207,7 @@ class HandlerDateSpinner(
|
|||||||
return sdf.parse(dateString)
|
return sdf.parse(dateString)
|
||||||
}
|
}
|
||||||
|
|
||||||
// —— Textgröße prozentual zur Bildschirmhöhe (in sp) ——
|
// Textgröße prozentual zur Bildschirmhöhe (in sp)
|
||||||
private fun setTextSizePercentOfScreenHeight(view: TextView, percentOfHeight: Float) {
|
private fun setTextSizePercentOfScreenHeight(view: TextView, percentOfHeight: Float) {
|
||||||
val dm = (view.context ?: layout.context).resources.displayMetrics
|
val dm = (view.context ?: layout.context).resources.displayMetrics
|
||||||
val sp = (dm.heightPixels * percentOfHeight) / dm.scaledDensity
|
val sp = (dm.heightPixels * percentOfHeight) / dm.scaledDensity
|
||||||
@ -210,7 +215,7 @@ class HandlerDateSpinner(
|
|||||||
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, sp)
|
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, sp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// —— Spinner-Adapter: Schrift & Zeilenhöhe dynamisch, kein Abschneiden ——
|
// 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 dm = context.resources.displayMetrics
|
val dm = context.resources.displayMetrics
|
||||||
|
|
||||||
@ -246,13 +251,13 @@ class HandlerDateSpinner(
|
|||||||
|
|
||||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||||
val v = super.getView(position, convertView, parent) as TextView
|
val v = super.getView(position, convertView, parent) as TextView
|
||||||
styleRow(v, forceHeight = false) // ausgewählte Ansicht
|
styleRow(v, forceHeight = false)
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
|
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||||
val v = super.getDropDownView(position, convertView, parent) as TextView
|
val v = super.getDropDownView(position, convertView, parent) as TextView
|
||||||
styleRow(v, forceHeight = true) // Dropdown-Zeilen: Höhe erzwingen
|
styleRow(v, forceHeight = true)
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -260,7 +265,6 @@ class HandlerDateSpinner(
|
|||||||
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
|
|
||||||
spinner.setPadding(spinner.paddingLeft, vPadPx, spinner.paddingRight, vPadPx)
|
spinner.setPadding(spinner.paddingLeft, vPadPx, spinner.paddingRight, vPadPx)
|
||||||
spinner.minimumHeight = rowHeight
|
spinner.minimumHeight = rowHeight
|
||||||
spinner.requestLayout()
|
spinner.requestLayout()
|
||||||
|
|||||||
@ -8,6 +8,14 @@ import android.widget.*
|
|||||||
import androidx.core.widget.TextViewCompat
|
import androidx.core.widget.TextViewCompat
|
||||||
import kotlinx.coroutines.*
|
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,
|
||||||
private val answers: MutableMap<String, Any>,
|
private val answers: MutableMap<String, Any>,
|
||||||
@ -60,7 +68,7 @@ class HandlerGlassScaleQuestion(
|
|||||||
setTextSizePercentOfScreenHeight(titleTv, 0.03f)
|
setTextSizePercentOfScreenHeight(titleTv, 0.03f)
|
||||||
setTextSizePercentOfScreenHeight(questionTv, 0.03f)
|
setTextSizePercentOfScreenHeight(questionTv, 0.03f)
|
||||||
|
|
||||||
// ----- feste Icon-Leiste -----
|
// feste Icon-Leiste
|
||||||
val header = layout.findViewById<LinearLayout>(R.id.glass_header)
|
val header = layout.findViewById<LinearLayout>(R.id.glass_header)
|
||||||
header.removeAllViews()
|
header.removeAllViews()
|
||||||
header.addView(Space(context).apply {
|
header.addView(Space(context).apply {
|
||||||
@ -80,7 +88,7 @@ class HandlerGlassScaleQuestion(
|
|||||||
cell.addView(img)
|
cell.addView(img)
|
||||||
header.addView(cell)
|
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()
|
||||||
@ -156,7 +164,7 @@ class HandlerGlassScaleQuestion(
|
|||||||
layoutParams = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT, 5f)
|
layoutParams = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT, 5f)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WICHTIG: RadioButtons sind direkte Kinder des RadioGroup!
|
// RadioButtons sind direkte Kinder der RadioGroup!
|
||||||
scaleLabels.forEach { labelKey ->
|
scaleLabels.forEach { labelKey ->
|
||||||
val rb = RadioButton(context).apply {
|
val rb = RadioButton(context).apply {
|
||||||
tag = labelKey
|
tag = labelKey
|
||||||
@ -215,7 +223,6 @@ class HandlerGlassScaleQuestion(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Helpers ---
|
|
||||||
private fun getRadioFromChild(child: View): RadioButton? =
|
private fun getRadioFromChild(child: View): RadioButton? =
|
||||||
when (child) {
|
when (child) {
|
||||||
is RadioButton -> child
|
is RadioButton -> child
|
||||||
|
|||||||
@ -9,6 +9,21 @@ import android.widget.TextView
|
|||||||
import androidx.core.widget.TextViewCompat
|
import androidx.core.widget.TextViewCompat
|
||||||
import com.google.android.material.button.MaterialButton
|
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>,
|
||||||
private val languageID: String,
|
private val languageID: String,
|
||||||
@ -19,7 +34,7 @@ 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
|
||||||
@ -81,7 +96,6 @@ class HandlerLastPage(
|
|||||||
override fun validate(): Boolean = true
|
override fun validate(): Boolean = true
|
||||||
override fun saveAnswer() {}
|
override fun saveAnswer() {}
|
||||||
|
|
||||||
// ---------- Responsive Textgröße für den Finish-Button ----------
|
|
||||||
private fun applyResponsiveTextSizing(btn: MaterialButton) {
|
private fun applyResponsiveTextSizing(btn: MaterialButton) {
|
||||||
// Max-/Min-Sp anhand der Bildschirmhöhe (in sp) berechnen
|
// Max-/Min-Sp anhand der Bildschirmhöhe (in sp) berechnen
|
||||||
val dm = btn.resources.displayMetrics
|
val dm = btn.resources.displayMetrics
|
||||||
|
|||||||
@ -7,6 +7,11 @@ import kotlinx.coroutines.*
|
|||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import androidx.core.widget.TextViewCompat
|
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,
|
||||||
private val answers: MutableMap<String, Any>,
|
private val answers: MutableMap<String, Any>,
|
||||||
@ -15,7 +20,7 @@ class HandlerMultiCheckboxQuestion(
|
|||||||
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-ID wie bei den anderen Handlern
|
private val questionnaireMeta: String //
|
||||||
) : QuestionHandler {
|
) : QuestionHandler {
|
||||||
|
|
||||||
private lateinit var layout: View
|
private lateinit var layout: View
|
||||||
@ -29,14 +34,12 @@ 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)
|
||||||
|
|
||||||
// Texte setzen
|
|
||||||
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) =====
|
// Textgrößen pro Bildschirmhöhe (wie bei deinen anderen Handlern)
|
||||||
setTextSizePercentOfScreenHeight(questionTextView, 0.03f) // Überschrift
|
setTextSizePercentOfScreenHeight(questionTextView, 0.03f) // Überschrift
|
||||||
setTextSizePercentOfScreenHeight(questionTitle, 0.03f) // Frage
|
setTextSizePercentOfScreenHeight(questionTitle, 0.03f) // Frage
|
||||||
// ==========================================================================
|
|
||||||
|
|
||||||
container.removeAllViews()
|
container.removeAllViews()
|
||||||
|
|
||||||
@ -45,13 +48,12 @@ class HandlerMultiCheckboxQuestion(
|
|||||||
(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) ———
|
// Checkbox-Schrift & Zeilenhöhe dynamisch ableiten (kein Abschneiden)
|
||||||
val dm = layout.resources.displayMetrics
|
val dm = layout.resources.displayMetrics
|
||||||
val cbTextSp = (dm.heightPixels * 0.025f) / dm.scaledDensity // ~2.5% der Bildschirmhöhe
|
val cbTextSp = (dm.heightPixels * 0.025f) / dm.scaledDensity // ~2.5% der Bildschirmhöhe
|
||||||
val cbTextPx = cbTextSp * dm.scaledDensity
|
val cbTextPx = cbTextSp * dm.scaledDensity
|
||||||
val cbPadV = (cbTextPx * 0.40f).toInt()
|
val cbPadV = (cbTextPx * 0.40f).toInt()
|
||||||
val cbMinH = (cbTextPx * 1.60f + 2 * cbPadV).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 {
|
||||||
@ -78,7 +80,7 @@ class HandlerMultiCheckboxQuestion(
|
|||||||
container.addView(checkBox)
|
container.addView(checkBox)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- DB-Abfrage falls noch kein Eintrag im answers-Map existiert ---
|
//DB-Abfrage falls noch kein Eintrag im answers-Map existiert
|
||||||
val answerMapKey = question.question ?: (question.id ?: "")
|
val answerMapKey = question.question ?: (question.id ?: "")
|
||||||
if (answerMapKey.isNotBlank() && !answers.containsKey(answerMapKey)) {
|
if (answerMapKey.isNotBlank() && !answers.containsKey(answerMapKey)) {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
@ -100,7 +102,7 @@ class HandlerMultiCheckboxQuestion(
|
|||||||
cb.isChecked = parsed.contains(cb.tag.toString())
|
cb.isChecked = parsed.contains(cb.tag.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
// answers-Map aktualisieren (Liste)
|
// answers-Map aktualisieren
|
||||||
answers[answerMapKey] = parsed.toList()
|
answers[answerMapKey] = parsed.toList()
|
||||||
|
|
||||||
// Punkte berechnen und hinzufügen
|
// Punkte berechnen und hinzufügen
|
||||||
@ -175,16 +177,8 @@ class HandlerMultiCheckboxQuestion(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Parsen der DB-Antwort in ein Set von Keys. Unterstützt:
|
|
||||||
* - JSON-Array: ["a","b"]
|
|
||||||
* - kommasepariert: a,b
|
|
||||||
* - semikolon-separiert: a;b
|
|
||||||
* - einzelner Wert: a
|
|
||||||
*/
|
|
||||||
private fun parseMultiAnswer(dbAnswer: String): Set<String> {
|
private fun parseMultiAnswer(dbAnswer: String): Set<String> {
|
||||||
val trimmed = dbAnswer.trim()
|
val trimmed = dbAnswer.trim()
|
||||||
// JSON-Array-like
|
|
||||||
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
||||||
val inner = trimmed.substring(1, trimmed.length - 1)
|
val inner = trimmed.substring(1, trimmed.length - 1)
|
||||||
if (inner.isBlank()) return emptySet()
|
if (inner.isBlank()) return emptySet()
|
||||||
@ -194,7 +188,6 @@ class HandlerMultiCheckboxQuestion(
|
|||||||
.toSet()
|
.toSet()
|
||||||
}
|
}
|
||||||
|
|
||||||
// If contains comma or semicolon
|
|
||||||
val separator = when {
|
val separator = when {
|
||||||
trimmed.contains(",") -> ","
|
trimmed.contains(",") -> ","
|
||||||
trimmed.contains(";") -> ";"
|
trimmed.contains(";") -> ";"
|
||||||
@ -211,7 +204,6 @@ class HandlerMultiCheckboxQuestion(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper: Textgröße prozentual zur Bildschirmhöhe setzen
|
|
||||||
private fun setTextSizePercentOfScreenHeight(view: TextView, percentOfHeight: Float) {
|
private fun setTextSizePercentOfScreenHeight(view: TextView, percentOfHeight: Float) {
|
||||||
val dm = (view.context ?: layout.context).resources.displayMetrics
|
val dm = (view.context ?: layout.context).resources.displayMetrics
|
||||||
val sp = (dm.heightPixels * percentOfHeight) / dm.scaledDensity
|
val sp = (dm.heightPixels * percentOfHeight) / dm.scaledDensity
|
||||||
|
|||||||
@ -441,16 +441,53 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
|
|||||||
private fun setupUploadButton() {
|
private fun setupUploadButton() {
|
||||||
uploadButton.text = t("upload")
|
uploadButton.text = t("upload")
|
||||||
uploadButton.setOnClickListener {
|
uploadButton.setOnClickListener {
|
||||||
val token = TokenStore.getToken(activity)
|
// 1) Token-Frische prüfen (24h Gültigkeit; wir nehmen 23h als Puffer)
|
||||||
if (token.isNullOrBlank()) {
|
val existingToken = TokenStore.getToken(activity)
|
||||||
|
val ageMs = System.currentTimeMillis() - TokenStore.getLoginTimestamp(activity)
|
||||||
|
val isFresh = !existingToken.isNullOrBlank() && ageMs < 23 * 60 * 60 * 1000
|
||||||
|
|
||||||
|
if (isFresh) {
|
||||||
|
// Frischer Token -> direkt hochladen
|
||||||
|
GlobalValues.LAST_CLIENT_CODE = editText.text.toString().trim()
|
||||||
|
DatabaseUploader.uploadDatabaseWithToken(activity, existingToken!!)
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Kein/alter Token -> Login anstoßen
|
||||||
|
val username = TokenStore.getUsername(activity)?.trim().orEmpty()
|
||||||
|
if (username.isBlank()) {
|
||||||
Toast.makeText(activity, t("login_required"), Toast.LENGTH_LONG).show()
|
Toast.makeText(activity, t("login_required"), Toast.LENGTH_LONG).show()
|
||||||
return@setOnClickListener
|
return@setOnClickListener
|
||||||
}
|
}
|
||||||
GlobalValues.LAST_CLIENT_CODE = editText.text.toString().trim()
|
|
||||||
DatabaseUploader.uploadDatabaseWithToken(activity, token)
|
// *** NUR für deine Testumgebung: bekannte Passwörter ableiten ***
|
||||||
|
// user01 -> pw1, user02 -> pw2 (entspricht login.php)
|
||||||
|
val password = when (username) {
|
||||||
|
"user01" -> "pw1"
|
||||||
|
"user02" -> "pw2"
|
||||||
|
else -> {
|
||||||
|
Toast.makeText(activity, t("login_required"), Toast.LENGTH_LONG).show()
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-Login -> neuen Token speichern -> Upload starten
|
||||||
|
LoginManager.loginUserWithCredentials(
|
||||||
|
context = activity,
|
||||||
|
username = username,
|
||||||
|
password = password,
|
||||||
|
onSuccess = { freshToken ->
|
||||||
|
GlobalValues.LAST_CLIENT_CODE = editText.text.toString().trim()
|
||||||
|
DatabaseUploader.uploadDatabaseWithToken(activity, freshToken)
|
||||||
|
},
|
||||||
|
onError = { msg ->
|
||||||
|
Toast.makeText(activity, t("login_failed_with_reason").replace("{reason}", msg), Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun setupDownloadButton() {
|
private fun setupDownloadButton() {
|
||||||
downloadButton.text = t("download")
|
downloadButton.text = t("download")
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,11 @@ import kotlinx.coroutines.*
|
|||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import androidx.core.widget.TextViewCompat // <— hinzugefügt
|
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,
|
||||||
private val answers: MutableMap<String, Any>,
|
private val answers: MutableMap<String, Any>,
|
||||||
@ -36,7 +41,7 @@ class HandlerRadioQuestion(
|
|||||||
Html.fromHtml(LanguageManager.getText(languageID, it), Html.FROM_HTML_MODE_LEGACY)
|
Html.fromHtml(LanguageManager.getText(languageID, it), Html.FROM_HTML_MODE_LEGACY)
|
||||||
} ?: ""
|
} ?: ""
|
||||||
|
|
||||||
// === Schriftgrößen wie im HandlerClientCoachCode ===
|
//
|
||||||
// Titel/Frage: 3% der Bildschirmhöhe
|
// Titel/Frage: 3% der Bildschirmhöhe
|
||||||
setTextSizePercentOfScreenHeight(questionTextView, 0.03f)
|
setTextSizePercentOfScreenHeight(questionTextView, 0.03f)
|
||||||
setTextSizePercentOfScreenHeight(questionTitle, 0.03f)
|
setTextSizePercentOfScreenHeight(questionTitle, 0.03f)
|
||||||
@ -132,7 +137,7 @@ class HandlerRadioQuestion(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ——— Helper: setzt Textgröße prozentual zur Bildschirmhöhe (in sp) ———
|
// setzt Textgröße prozentual zur Bildschirmhöhe (in sp)
|
||||||
private fun setTextSizePercentOfScreenHeight(view: TextView, percentOfHeight: Float) {
|
private fun setTextSizePercentOfScreenHeight(view: TextView, percentOfHeight: Float) {
|
||||||
val dm = (view.context ?: layout.context).resources.displayMetrics
|
val dm = (view.context ?: layout.context).resources.displayMetrics
|
||||||
val sp = (dm.heightPixels * percentOfHeight) / dm.scaledDensity
|
val sp = (dm.heightPixels * percentOfHeight) / dm.scaledDensity
|
||||||
|
|||||||
@ -9,6 +9,12 @@ import android.util.TypedValue
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.widget.TextViewCompat
|
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,
|
||||||
private val answers: MutableMap<String, Any>,
|
private val answers: MutableMap<String, Any>,
|
||||||
@ -16,7 +22,7 @@ class HandlerStringSpinner(
|
|||||||
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 // Meta, damit dieselbe DB-ID wie in anderen Handlern gebildet wird
|
private val questionnaireMeta: String
|
||||||
) : QuestionHandler {
|
) : QuestionHandler {
|
||||||
|
|
||||||
private lateinit var layout: View
|
private lateinit var layout: View
|
||||||
@ -36,20 +42,19 @@ class HandlerStringSpinner(
|
|||||||
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) ===
|
// Textgrößen prozentual zur Bildschirmhöhe (wie im HandlerRadioQuestion)
|
||||||
setTextSizePercentOfScreenHeight(textView, 0.03f)
|
setTextSizePercentOfScreenHeight(textView, 0.03f)
|
||||||
setTextSizePercentOfScreenHeight(questionTextView, 0.03f)
|
setTextSizePercentOfScreenHeight(questionTextView, 0.03f)
|
||||||
// ==============================================================================
|
|
||||||
|
|
||||||
val options = buildOptionsList()
|
val options = buildOptionsList()
|
||||||
|
|
||||||
// vorhandene Auswahl (falls vorhanden)
|
// vorhandene Auswahl (falls vorhanden)
|
||||||
val savedSelection = question.question?.let { answers[it] as? String }
|
val savedSelection = question.question?.let { answers[it] as? String }
|
||||||
|
|
||||||
// Spinner aufsetzen (Schriftgröße & Zeilenhöhe dynamisch, kein Abschneiden)
|
// Spinner aufsetzen
|
||||||
setupSpinner(spinner, options, savedSelection)
|
setupSpinner(spinner, options, savedSelection)
|
||||||
|
|
||||||
// Falls noch keine Antwort im Map: aus DB laden (analog zu anderen Handlern)
|
// Falls noch keine Antwort im Map: aus DB laden
|
||||||
val answerMapKey = question.question ?: (question.id ?: "")
|
val answerMapKey = question.question ?: (question.id ?: "")
|
||||||
if (answerMapKey.isNotBlank() && !answers.containsKey(answerMapKey)) {
|
if (answerMapKey.isNotBlank() && !answers.containsKey(answerMapKey)) {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
|||||||
@ -8,6 +8,14 @@ import kotlinx.coroutines.*
|
|||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import androidx.core.widget.TextViewCompat // <- NEU
|
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,
|
||||||
private val answers: MutableMap<String, Any>,
|
private val answers: MutableMap<String, Any>,
|
||||||
@ -16,7 +24,7 @@ class HandlerValueSpinner(
|
|||||||
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 // neu: für die DB-Abfrage
|
private val questionnaireMeta: String
|
||||||
) : QuestionHandler {
|
) : QuestionHandler {
|
||||||
|
|
||||||
private lateinit var layout: View
|
private lateinit var layout: View
|
||||||
@ -35,11 +43,10 @@ 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 ===
|
// Schriftgrößen wie im HandlerRadioQuestion
|
||||||
// Titel/Frage: 3% der Bildschirmhöhe
|
// Titel/Frage: 3% der Bildschirmhöhe
|
||||||
setTextSizePercentOfScreenHeight(textView, 0.03f)
|
setTextSizePercentOfScreenHeight(textView, 0.03f)
|
||||||
setTextSizePercentOfScreenHeight(questionTextView, 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) {
|
||||||
@ -51,7 +58,7 @@ 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 ---
|
//DB-Abfrage falls noch keine Antwort im Map existiert
|
||||||
val answerMapKey = question.question ?: (question.id ?: "")
|
val answerMapKey = question.question ?: (question.id ?: "")
|
||||||
if (answerMapKey.isNotBlank() && !answers.containsKey(answerMapKey)) {
|
if (answerMapKey.isNotBlank() && !answers.containsKey(answerMapKey)) {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
@ -120,14 +127,13 @@ class HandlerValueSpinner(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ——— Helper: setzt Textgröße prozentual zur Bildschirmhöhe (in sp) ———
|
// setzt Textgröße prozentual zur Bildschirmhöhe (in sp)
|
||||||
private fun setTextSizePercentOfScreenHeight(view: TextView, percentOfHeight: Float) {
|
private fun setTextSizePercentOfScreenHeight(view: TextView, percentOfHeight: Float) {
|
||||||
val dm = (view.context ?: layout.context).resources.displayMetrics
|
val dm = (view.context ?: layout.context).resources.displayMetrics
|
||||||
val sp = (dm.heightPixels * percentOfHeight) / dm.scaledDensity
|
val sp = (dm.heightPixels * percentOfHeight) / dm.scaledDensity
|
||||||
TextViewCompat.setAutoSizeTextTypeWithDefaults(view, TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE)
|
TextViewCompat.setAutoSizeTextTypeWithDefaults(view, TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE)
|
||||||
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, sp)
|
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 dm = context.resources.displayMetrics
|
val dm = context.resources.displayMetrics
|
||||||
|
|||||||
@ -7,6 +7,12 @@ import org.apache.poi.xssf.usermodel.XSSFWorkbook
|
|||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import java.nio.charset.Charset
|
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(
|
class HeaderOrderRepository(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
// Sprache abrufen (Standard: Deutsch, damit es ohne OpeningScreen schon sinnvoll ist)
|
// Sprache abrufen (Standard: Deutsch, damit es ohne OpeningScreen schon sinnvoll ist)
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -4,6 +4,20 @@ import android.content.Context
|
|||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
import android.net.NetworkCapabilities
|
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:
|
||||||
|
- Vor Netzwerkaufrufen (Login, Upload, Download) aufrufen, um „Offline“-Fälle frühzeitig abzufangen und nutzerfreundliche Meldungen zu zeigen.
|
||||||
|
*/
|
||||||
object NetworkUtils {
|
object NetworkUtils {
|
||||||
fun isOnline(context: Context): Boolean {
|
fun isOnline(context: Context): Boolean {
|
||||||
return try {
|
return try {
|
||||||
|
|||||||
@ -2,20 +2,32 @@ package com.dano.test1
|
|||||||
|
|
||||||
import android.content.Context
|
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 {
|
object TokenStore {
|
||||||
private const val PREF = "qdb_prefs"
|
private const val PREF = "qdb_prefs"
|
||||||
private const val KEY_TOKEN = "token"
|
private const val KEY_TOKEN = "token"
|
||||||
private const val KEY_USER = "user"
|
private const val KEY_USER = "user"
|
||||||
private const val KEY_LOGIN_TS = "login_ts"
|
private const val KEY_LOGIN_TS = "login_ts"
|
||||||
|
|
||||||
/** Speichert Token, Username und Login-Timestamp (jetzt) */
|
|
||||||
fun save(context: Context, token: String, username: String) {
|
fun save(context: Context, token: String, username: String) {
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
context.getSharedPreferences(PREF, Context.MODE_PRIVATE)
|
context.getSharedPreferences(PREF, Context.MODE_PRIVATE)
|
||||||
.edit()
|
.edit()
|
||||||
.putString(KEY_TOKEN, token)
|
.putString(KEY_TOKEN, token) // API-/Session-Token
|
||||||
.putString(KEY_USER, username)
|
.putString(KEY_USER, username) // angemeldeter Benutzername
|
||||||
.putLong(KEY_LOGIN_TS, now)
|
.putLong(KEY_LOGIN_TS, now) // Zeitpunkt des Logins
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user