changed questionnaire_order.json framework.

This commit is contained in:
oxidiert
2025-08-18 13:36:08 +02:00
parent 6e33d61b1e
commit 148af18496
4 changed files with 252 additions and 83 deletions

View File

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

View File

@ -69,6 +69,11 @@ class HandlerClientCoachCode(
val clientCode = clientCodeField.text.toString() val clientCode = clientCodeField.text.toString()
val coachCode = coachCodeField.text.toString() val coachCode = coachCodeField.text.toString()
// Prüfen, ob die Datenbank-Dateien vor dem Klick existieren
val dbFile = layout.context.getDatabasePath("questionnaire_database")
val dbJournalFile = layout.context.getDatabasePath("questionnaire_database-journal")
val dbExisted = dbFile.exists() || dbJournalFile.exists()
// Check if client code already exists asynchronously // 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)
@ -81,6 +86,13 @@ class HandlerClientCoachCode(
} else { } else {
// Either no existing client or re-using previous code // 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()
} }
} }

View File

@ -8,10 +8,11 @@ import android.view.View
import android.widget.* import android.widget.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject
import android.util.Log import android.util.Log
import com.dano.test1.data.CompletedQuestionnaire
import java.io.File import java.io.File
var INTEGRATION_INDEX_POINTS: Int? = null var INTEGRATION_INDEX_POINTS: Int? = null
class HandlerOpeningScreen(private val activity: MainActivity) { class HandlerOpeningScreen(private val activity: MainActivity) {
@ -52,8 +53,7 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
val pathExists = File(dbPath).exists() val pathExists = File(dbPath).exists()
if (pathExists) { if (pathExists) {
updateMainButtonsState(true) updateMainButtonsState(true)
} } else {
else{
updateMainButtonsState(false) updateMainButtonsState(false)
} }
@ -90,15 +90,7 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
val obj = jsonArray.getJSONObject(i) val obj = jsonArray.getJSONObject(i)
val file = obj.getString("file") val file = obj.getString("file")
val conditionObj = obj.optJSONObject("condition") val conditionObj = obj.optJSONObject("condition")
val condition = if (conditionObj != null) { val condition = parseCondition(conditionObj)
QuestionItem.Condition(
questionnaire = conditionObj.getString("questionnaire"),
questionId = conditionObj.getString("questionId"),
operator = conditionObj.getString("operator"),
value = conditionObj.getString("value")
)
} else null
val showPoints = obj.optBoolean("showPoints", false) val showPoints = obj.optBoolean("showPoints", false)
QuestionItem.QuestionnaireEntry(file, condition, showPoints) QuestionItem.QuestionnaireEntry(file, condition, showPoints)
@ -109,6 +101,63 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
} }
} }
// Parser: erzeugt ein QuestionItem.Condition? (sehr robust gegenüber verschiedenen JSON-Formaten)
private fun parseCondition(conditionObj: JSONObject?): QuestionItem.Condition? {
if (conditionObj == null) return null
// anyOf
if (conditionObj.has("anyOf")) {
val arr = conditionObj.optJSONArray("anyOf") ?: JSONArray()
val conditions = mutableListOf<QuestionItem.Condition>()
for (i in 0 until arr.length()) {
val sub = arr.optJSONObject(i)
parseCondition(sub)?.let { conditions.add(it) }
}
return QuestionItem.Condition.AnyOf(conditions)
}
// alwaysAvailable
if (conditionObj.has("alwaysAvailable")) {
val flag = conditionObj.optBoolean("alwaysAvailable", false)
if (flag) return QuestionItem.Condition.AlwaysAvailable
}
// requiresCompleted (array)
val requiresList = mutableListOf<String>()
if (conditionObj.has("requiresCompleted")) {
val reqArr = conditionObj.optJSONArray("requiresCompleted")
if (reqArr != null) {
for (i in 0 until reqArr.length()) {
requiresList.add(reqArr.optString(i))
}
} else {
// sometimes it's a single string
conditionObj.optString("requiresCompleted")?.let { if (it.isNotBlank()) requiresList.add(it) }
}
}
// question-check fields
val questionnaire = conditionObj.optString("questionnaire", null)
val questionId = conditionObj.optString("questionId", null)
val operator = conditionObj.optString("operator", null)
val value = conditionObj.optString("value", null)
val hasQuestionCheck = !questionnaire.isNullOrBlank() && !questionId.isNullOrBlank() && !operator.isNullOrBlank() && value != null
return when {
requiresList.isNotEmpty() && hasQuestionCheck -> {
QuestionItem.Condition.Combined(requiresList, QuestionItem.Condition.QuestionCondition(questionnaire!!, questionId!!, operator!!, value!!))
}
hasQuestionCheck -> {
QuestionItem.Condition.QuestionCondition(questionnaire!!, questionId!!, operator!!, value!!)
}
requiresList.isNotEmpty() -> {
QuestionItem.Condition.RequiresCompleted(requiresList)
}
else -> null
}
}
private fun createQuestionnaireButtons() { private fun createQuestionnaireButtons() {
buttonContainer.removeAllViews() buttonContainer.removeAllViews()
dynamicButtons.clear() dynamicButtons.clear()
@ -129,12 +178,23 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
} }
updateButtonTexts() updateButtonTexts()
setButtonsEnabled(listOf(dynamicButtons.firstOrNull()).filterNotNull())
// Initial: enable those with AlwaysAvailable (falls vorhanden)
val alwaysButtons = questionnaireEntries.mapIndexedNotNull { idx, entry ->
val btn = dynamicButtons.getOrNull(idx)
if (entry.condition is QuestionItem.Condition.AlwaysAvailable) btn else null
}
setButtonsEnabled(alwaysButtons)
dynamicButtons.forEach { button -> dynamicButtons.forEach { button ->
button.setOnClickListener { button.setOnClickListener {
// require a client code to start actual questionnaire (sichere Kontrolle)
val clientCode = editText.text.toString().trim()
GlobalValues.LAST_CLIENT_CODE = clientCode
startQuestionnaireForButton(button) startQuestionnaireForButton(button)
setButtonsEnabled(dynamicButtons.filter { it != button }) // disable other buttons while one questionnaire is open
setButtonsEnabled(dynamicButtons.filter { it == button })
} }
} }
} }
@ -190,86 +250,143 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
val message = LanguageManager.getText(languageID, "no_profile") val message = LanguageManager.getText(languageID, "no_profile")
Toast.makeText(activity, message, Toast.LENGTH_LONG).show() Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
setButtonsEnabled(listOf(dynamicButtons.firstOrNull()).filterNotNull()) // enable only alwaysAvailable ones if no client found
val alwaysButtons = questionnaireEntries.mapIndexedNotNull { idx, entry ->
val btn = dynamicButtons.getOrNull(idx)
if (entry.condition is QuestionItem.Condition.AlwaysAvailable) btn else null
}
setButtonsEnabled(alwaysButtons)
} }
return@launch return@launch
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
updateMainButtonsState(true) // Datenbank vorhanden -> Buttons aktivieren updateMainButtonsState(true) // Datenbank vorhanden -> Buttons aktivieren
handleNormalLoad(clientCode) }
handleNormalLoad(clientCode)
}
}
// Evaluierung der Bedingung: suspend, weil DB-Abfragen stattfinden.
private suspend fun evaluateCondition(
condition: QuestionItem.Condition?,
clientCode: String,
completedEntries: List<CompletedQuestionnaire> // Anpassung an deinem DAO-Objekt-Name
): Boolean {
if (condition == null) return false
when (condition) {
is QuestionItem.Condition.AlwaysAvailable -> return true
is QuestionItem.Condition.RequiresCompleted -> {
// prüfen, ob alle required items in completedEntries vorhanden und isDone == true sind
val normalizedCompleted = completedEntries.map { normalizeQuestionnaireId(it.questionnaireId) }
return condition.required.all { req ->
val nReq = normalizeQuestionnaireId(req)
normalizedCompleted.any { it.contains(nReq) || nReq.contains(it) }
}
}
is QuestionItem.Condition.QuestionCondition -> {
// need to fetch the answer for that questionnaire/questionId
val answers = MyApp.database.answerDao().getAnswersForClientAndQuestionnaire(clientCode, condition.questionnaire)
val relevant = answers.find { it.questionId.endsWith(condition.questionId, ignoreCase = true) }
val answerValue = relevant?.answerValue ?: ""
return when (condition.operator) {
"==" -> answerValue == condition.value
"!=" -> answerValue != condition.value
else -> false
}
}
is QuestionItem.Condition.Combined -> {
// Combined: requiresCompleted (if present) AND questionCheck must match
val reqOk = if (condition.requiresCompleted.isNullOrEmpty()) true
else {
val normalizedCompleted = completedEntries.map { normalizeQuestionnaireId(it.questionnaireId) }
condition.requiresCompleted.all { req ->
val nReq = normalizeQuestionnaireId(req)
normalizedCompleted.any { it.contains(nReq) || nReq.contains(it) }
}
}
if (!reqOk) return false
// dann Frage-Check auswerten
val q = condition.questionCheck
if (q != null) {
val answers = MyApp.database.answerDao().getAnswersForClientAndQuestionnaire(clientCode, q.questionnaire)
val relevant = answers.find { it.questionId.endsWith(q.questionId, ignoreCase = true) }
val answerValue = relevant?.answerValue ?: ""
return when (q.operator) {
"==" -> answerValue == q.value
"!=" -> answerValue != q.value
else -> false
}
}
return reqOk
}
is QuestionItem.Condition.AnyOf -> {
// true, wenn irgendeine der Sub-Bedingungen erfüllt ist
for (sub in condition.conditions) {
val subRes = evaluateCondition(sub, clientCode, completedEntries)
if (subRes) return true
}
return false
} }
} }
} }
private fun normalizeQuestionnaireId(name: String): String {
return name.lowercase().removeSuffix(".json")
}
private suspend fun handleNormalLoad(clientCode: String) { private suspend fun handleNormalLoad(clientCode: String) {
val completedIds = withContext(Dispatchers.IO) {
MyApp.database.completedQuestionnaireDao().getCompletedQuestionnairesForClient(clientCode)
}
if (completedIds.isEmpty()) {
setButtonsEnabled(listOf(dynamicButtons.firstOrNull()).filterNotNull())
val message = LanguageManager.getText(languageID, "no_profile")
Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
return
}
val completedIndexes = completedIds.mapNotNull { id ->
questionnaireEntries.indexOfFirst { it.file.contains(id, ignoreCase = true) }.takeIf { it >= 0 }
}.sorted()
val completedEntries = withContext(Dispatchers.IO) { val completedEntries = withContext(Dispatchers.IO) {
MyApp.database.completedQuestionnaireDao().getAllForClient(clientCode) MyApp.database.completedQuestionnaireDao().getAllForClient(clientCode)
} }
// fülle buttonPoints & INTEGRATION_INDEX_POINTS
buttonPoints.clear() buttonPoints.clear()
for (entry in completedEntries) { for (entry in completedEntries) {
if (entry.isDone) { if (entry.isDone) {
buttonPoints[entry.questionnaireId] = entry.sumPoints ?: 0 buttonPoints[entry.questionnaireId] = entry.sumPoints ?: 0
if (entry.questionnaireId.contains("questionnaire_3_integration_index", ignoreCase = true)) { if (entry.questionnaireId.contains("questionnaire_3_integration_index", ignoreCase = true)) {
INTEGRATION_INDEX_POINTS = entry.sumPoints INTEGRATION_INDEX_POINTS = entry.sumPoints
} }
} }
} }
updateButtonTexts() withContext(Dispatchers.Main) {
updateButtonTexts()
var nextIndex = (completedIndexes.lastOrNull() ?: -1) + 1
while (nextIndex < questionnaireEntries.size) {
val entry = questionnaireEntries[nextIndex]
val condition = entry.condition
if (condition != null) {
val answers = MyApp.database.answerDao().getAnswersForClientAndQuestionnaire(clientCode, condition.questionnaire)
val relevantAnswer = answers.find {
it.questionId.endsWith(condition.questionId)
}
val answerValue = relevantAnswer?.answerValue ?: ""
val conditionMet = when (condition.operator) {
"!=" -> answerValue != condition.value
"==" -> answerValue == condition.value
else -> true
}
if (conditionMet) break
else nextIndex++
} else {
break
}
} }
if (nextIndex >= questionnaireEntries.size) { // für jeden Fragebogen prüfen, ob er aktiv sein darf
setButtonsEnabled(emptyList()) val enabledButtons = mutableListOf<Button>()
val message = LanguageManager.getText(languageID, "questionnaires_finished") for ((idx, entry) in questionnaireEntries.withIndex()) {
Toast.makeText(activity, message, Toast.LENGTH_LONG).show() val button = dynamicButtons.getOrNull(idx) ?: continue
} else {
val nextFileName = questionnaireEntries[nextIndex].file // falls bereits erledigt: nicht anklickbar
val nextButton = questionnaireFiles.entries.firstOrNull { it.value == nextFileName }?.key val isCompleted = completedEntries.any { completed ->
setButtonsEnabled(listOfNotNull(nextButton)) normalizeQuestionnaireId(completed.questionnaireId).let { completedNorm ->
val targetNorm = normalizeQuestionnaireId(entry.file)
completedNorm.contains(targetNorm) || targetNorm.contains(completedNorm)
} && completed.isDone
}
if (isCompleted) {
// ausdrücklich deaktivieren
continue
}
// auswerten der Bedingung (suspend)
val condMet = evaluateCondition(entry.condition, clientCode, completedEntries)
if (condMet) enabledButtons.add(button)
}
withContext(Dispatchers.Main) {
if (enabledButtons.isEmpty()) {
setButtonsEnabled(emptyList())
val message = LanguageManager.getText(languageID, "questionnaires_finished")
Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
} else {
setButtonsEnabled(enabledButtons)
}
} }
} }
@ -278,6 +395,7 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
val entry = questionnaireEntries.firstOrNull { it.file == fileName } val entry = questionnaireEntries.firstOrNull { it.file == fileName }
// key ableiten
val key = fileName.substringAfter("questionnaire_").substringAfter("_").removeSuffix(".json") val key = fileName.substringAfter("questionnaire_").substringAfter("_").removeSuffix(".json")
var buttonText = LanguageManager.getText(languageID, key) var buttonText = LanguageManager.getText(languageID, key)
@ -574,15 +692,12 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
// --- Füge diese Funktion in deine Klasse ein --- // --- Füge diese Funktion in deine Klasse ein ---
private fun isDatabasePopulated(): Boolean { private fun isDatabasePopulated(): Boolean {
return try { return try {
// Wir prüfen, ob die Datenbank mindestens eine nicht-interne Tabelle enthält.
// Das ist robust gegenüber verschiedenen Tabellennamen.
val db = MyApp.database.openHelper.readableDatabase val db = MyApp.database.openHelper.readableDatabase
val cursor = db.query( val cursor = db.query(
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name != 'room_master_table'" "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name != 'room_master_table'"
) )
cursor.use { it.count > 0 } cursor.use { it.count > 0 }
} catch (e: Exception) { } catch (e: Exception) {
// Falls etwas schiefgeht (z.B. DB noch nicht vorhanden), gilt: nicht vorhanden
false false
} }
} }
@ -607,4 +722,4 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
button.alpha = if (isDatabaseAvailable) 1.0f else 0.5f button.alpha = if (isDatabaseAvailable) 1.0f else 0.5f
} }
} }
} }

View File

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