diff --git a/app/release/app-release.apk b/app/release/app-release.apk index 7fe80aa..8ca01d9 100644 Binary files a/app/release/app-release.apk and b/app/release/app-release.apk differ diff --git a/app/release/baselineProfiles/0/app-release.dm b/app/release/baselineProfiles/0/app-release.dm index 245bb70..8cad1a4 100644 Binary files a/app/release/baselineProfiles/0/app-release.dm and b/app/release/baselineProfiles/0/app-release.dm differ diff --git a/app/release/baselineProfiles/1/app-release.dm b/app/release/baselineProfiles/1/app-release.dm index 07a60d0..8c46e76 100644 Binary files a/app/release/baselineProfiles/1/app-release.dm and b/app/release/baselineProfiles/1/app-release.dm differ diff --git a/app/src/main/java/com/dano/test1/AES256Helper.kt b/app/src/main/java/com/dano/test1/AES256Helper.kt index d2f7f5f..cabc7f6 100644 --- a/app/src/main/java/com/dano/test1/AES256Helper.kt +++ b/app/src/main/java/com/dano/test1/AES256Helper.kt @@ -1,4 +1,3 @@ -// app/src/main/java/com/dano/test1/AES256Helper.kt package com.dano.test1 import java.io.File @@ -11,7 +10,6 @@ import kotlin.math.min 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 { val ikm = hexToBytes(tokenHex) val mac = Mac.getInstance("HmacSHA256") diff --git a/app/src/main/java/com/dano/test1/AppDatabase.kt b/app/src/main/java/com/dano/test1/AppDatabase.kt index e1f51be..e860a1e 100644 --- a/app/src/main/java/com/dano/test1/AppDatabase.kt +++ b/app/src/main/java/com/dano/test1/AppDatabase.kt @@ -3,6 +3,18 @@ package com.dano.test1.data import androidx.room.Database 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( entities = [ Client::class, diff --git a/app/src/main/java/com/dano/test1/Daos.kt b/app/src/main/java/com/dano/test1/Daos.kt index d7ac85e..268d5b5 100644 --- a/app/src/main/java/com/dano/test1/Daos.kt +++ b/app/src/main/java/com/dano/test1/Daos.kt @@ -2,8 +2,18 @@ package com.dano.test1.data 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 interface ClientDao { + @Insert(onConflict = OnConflictStrategy.IGNORE) suspend fun insertClient(client: Client) @@ -20,9 +30,9 @@ interface ClientDao { suspend fun getAllClients(): List } - @Dao interface QuestionnaireDao { + @Insert(onConflict = OnConflictStrategy.IGNORE) suspend fun insertQuestionnaire(questionnaire: Questionnaire) @@ -30,11 +40,12 @@ interface QuestionnaireDao { suspend fun getById(id: String): Questionnaire? @Query("SELECT * FROM questionnaires") - suspend fun getAll(): List // <-- NEU + suspend fun getAll(): List } @Dao interface QuestionDao { + @Insert(onConflict = OnConflictStrategy.IGNORE) suspend fun insertQuestions(questions: List) @@ -48,8 +59,10 @@ interface QuestionDao { suspend fun getQuestionsForQuestionnaire(questionnaireId: String): List } + @Dao interface AnswerDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertAnswers(answers: List) @@ -79,9 +92,9 @@ interface AnswerDao { ) } - @Dao interface CompletedQuestionnaireDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(entry: CompletedQuestionnaire) @@ -93,4 +106,4 @@ interface CompletedQuestionnaireDao { @Query("SELECT questionnaireId FROM completed_questionnaires WHERE clientCode = :clientCode") suspend fun getCompletedQuestionnairesForClient(clientCode: String): List -} \ No newline at end of file +} diff --git a/app/src/main/java/com/dano/test1/Entities.kt b/app/src/main/java/com/dano/test1/Entities.kt index 7aeebd5..cdb3880 100644 --- a/app/src/main/java/com/dano/test1/Entities.kt +++ b/app/src/main/java/com/dano/test1/Entities.kt @@ -2,16 +2,41 @@ package com.dano.test1.data 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") data class Client( @PrimaryKey val clientCode: String, ) +/* Tabelle: questionnaires – Eindeutige Fragebogen-IDs. */ @Entity(tableName = "questionnaires") data class Questionnaire( @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( tableName = "questions", foreignKeys = [ @@ -30,6 +55,12 @@ data class Question( 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( tableName = "answers", primaryKeys = ["clientCode", "questionId"], @@ -55,6 +86,17 @@ data class Answer( 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( tableName = "completed_questionnaires", primaryKeys = ["clientCode", "questionnaireId"], @@ -81,4 +123,3 @@ data class CompletedQuestionnaire( val isDone: Boolean, val sumPoints: Int? = null ) - diff --git a/app/src/main/java/com/dano/test1/ExcelExportService.kt b/app/src/main/java/com/dano/test1/ExcelExportService.kt index 523fdca..744f8af 100644 --- a/app/src/main/java/com/dano/test1/ExcelExportService.kt +++ b/app/src/main/java/com/dano/test1/ExcelExportService.kt @@ -10,12 +10,29 @@ 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". */ + /* Baut die Excel-Datei und speichert sie ausschließlich unter "Downloads". */ suspend fun exportHeadersForAllClients(): Uri? { val orderedIds = headerRepo.loadOrderedIds() 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? { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { val resolver = context.contentResolver @@ -108,7 +124,6 @@ class ExcelExportService( } } - // ---------- Export-spezifische Lokalisierung (EN) ---------- private suspend fun englishQuestionForId(id: String, questionnaireIdSet: Set): String { if (id == "client_code") return "Client code" if (id in questionnaireIdSet && !id.contains('-')) return "Questionnaire status" @@ -147,7 +162,7 @@ class ExcelExportService( 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 { if (id == "client_code") return raw if (raw == "Done" || raw == "Not Done" || raw == "None") return raw diff --git a/app/src/main/java/com/dano/test1/HandlerClientCoachCode.kt b/app/src/main/java/com/dano/test1/HandlerClientCoachCode.kt index 6beee3e..3103ba9 100644 --- a/app/src/main/java/com/dano/test1/HandlerClientCoachCode.kt +++ b/app/src/main/java/com/dano/test1/HandlerClientCoachCode.kt @@ -9,6 +9,11 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +/* +Zweck : +- Steuert die Eingabeseite für „Client Code“ und „Coach Code“ innerhalb des Fragebogen-Flows. +*/ + class HandlerClientCoachCode( private val answers: MutableMap, private val languageID: String, @@ -48,7 +53,7 @@ class HandlerClientCoachCode( 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) if (!coachFromLogin.isNullOrBlank()) { coachCodeField.setText(coachFromLogin) @@ -122,7 +127,6 @@ class HandlerClientCoachCode( override fun validate(): Boolean { val clientCode = layout.findViewById(R.id.client_code).text - // Validierung nimmt den (ggf. gesperrten) Text – passt val coachText = layout.findViewById(R.id.coach_code).text return clientCode.isNotBlank() && coachText.isNotBlank() } @@ -139,7 +143,6 @@ class HandlerClientCoachCode( // Not used } - // --- Helfer zum Sperren inkl. optischer Markierung (wie im Opening Screen) --- private fun lockCoachField(field: EditText) { field.isFocusable = false field.isFocusableInTouchMode = false diff --git a/app/src/main/java/com/dano/test1/HandlerClientNotSigned.kt b/app/src/main/java/com/dano/test1/HandlerClientNotSigned.kt index dbd7849..34b91f8 100644 --- a/app/src/main/java/com/dano/test1/HandlerClientNotSigned.kt +++ b/app/src/main/java/com/dano/test1/HandlerClientNotSigned.kt @@ -3,6 +3,12 @@ package com.dano.test1 import android.view.View 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( private val answers: MutableMap, private val languageID: String, @@ -14,7 +20,6 @@ class HandlerClientNotSigned( private lateinit var layout: View private lateinit var question: QuestionItem.ClientNotSigned - // UI components private lateinit var textView1: TextView private lateinit var textView2: TextView private lateinit var questionTextView: TextView @@ -26,29 +31,24 @@ class HandlerClientNotSigned( this.layout = layout this.question = question - // Initialize UI components only once + initViews() - // Set localized text values from LanguageManager textView1.text = question.textKey1?.let { LanguageManager.getText(languageID, it) } ?: "" textView2.text = question.textKey2?.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 ?: "") - // Set click listener for Next button layout.findViewById