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",
"showPoints": false
"showPoints": false,
"condition": {
"alwaysAvailable": true
}
},
{
"file": "questionnaire_2_rhs.json",
"showPoints": true
"showPoints": true,
"condition": {
"alwaysAvailable": true
}
},
{
"file": "questionnaire_3_integration_index.json",
"showPoints": true
"showPoints": true,
"condition": {
"alwaysAvailable": true
}
},
{
"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",
"showPoints": false,
"condition": {
"requiresCompleted": ["questionnaire_4_consultation_results"],
"questionnaire": "questionnaire_4_consultation_results",
"questionId": "consultation_decision",
"operator": "==",
@ -27,6 +44,20 @@
},
{
"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 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
CoroutineScope(Dispatchers.IO).launch {
val existingClient = MyApp.database.clientDao().getClientByCode(clientCode)
@ -81,6 +86,13 @@ class HandlerClientCoachCode(
} else {
// Either no existing client or re-using previous code
saveAnswers(clientCode, coachCode)
// Datenbank-Dateien löschen, wenn sie vorher NICHT existierten
if (!dbExisted) {
dbFile.delete()
dbJournalFile.delete()
}
goToNextQuestion()
}
}

View File

@ -8,10 +8,11 @@ import android.view.View
import android.widget.*
import kotlinx.coroutines.*
import org.json.JSONArray
import org.json.JSONObject
import android.util.Log
import com.dano.test1.data.CompletedQuestionnaire
import java.io.File
var INTEGRATION_INDEX_POINTS: Int? = null
class HandlerOpeningScreen(private val activity: MainActivity) {
@ -52,8 +53,7 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
val pathExists = File(dbPath).exists()
if (pathExists) {
updateMainButtonsState(true)
}
else{
} else {
updateMainButtonsState(false)
}
@ -90,15 +90,7 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
val obj = jsonArray.getJSONObject(i)
val file = obj.getString("file")
val conditionObj = obj.optJSONObject("condition")
val condition = if (conditionObj != null) {
QuestionItem.Condition(
questionnaire = conditionObj.getString("questionnaire"),
questionId = conditionObj.getString("questionId"),
operator = conditionObj.getString("operator"),
value = conditionObj.getString("value")
)
} else null
val condition = parseCondition(conditionObj)
val showPoints = obj.optBoolean("showPoints", false)
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() {
buttonContainer.removeAllViews()
dynamicButtons.clear()
@ -129,12 +178,23 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
}
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 ->
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)
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) {
val message = LanguageManager.getText(languageID, "no_profile")
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
}
withContext(Dispatchers.Main) {
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) {
val completedIds = withContext(Dispatchers.IO) {
MyApp.database.completedQuestionnaireDao().getCompletedQuestionnairesForClient(clientCode)
}
if (completedIds.isEmpty()) {
setButtonsEnabled(listOf(dynamicButtons.firstOrNull()).filterNotNull())
val message = LanguageManager.getText(languageID, "no_profile")
Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
return
}
val completedIndexes = completedIds.mapNotNull { id ->
questionnaireEntries.indexOfFirst { it.file.contains(id, ignoreCase = true) }.takeIf { it >= 0 }
}.sorted()
val completedEntries = withContext(Dispatchers.IO) {
MyApp.database.completedQuestionnaireDao().getAllForClient(clientCode)
}
// fülle buttonPoints & INTEGRATION_INDEX_POINTS
buttonPoints.clear()
for (entry in completedEntries) {
if (entry.isDone) {
buttonPoints[entry.questionnaireId] = entry.sumPoints ?: 0
if (entry.questionnaireId.contains("questionnaire_3_integration_index", ignoreCase = true)) {
INTEGRATION_INDEX_POINTS = entry.sumPoints
}
}
}
updateButtonTexts()
var nextIndex = (completedIndexes.lastOrNull() ?: -1) + 1
while (nextIndex < questionnaireEntries.size) {
val entry = questionnaireEntries[nextIndex]
val condition = entry.condition
if (condition != null) {
val answers = MyApp.database.answerDao().getAnswersForClientAndQuestionnaire(clientCode, condition.questionnaire)
val relevantAnswer = answers.find {
it.questionId.endsWith(condition.questionId)
}
val answerValue = relevantAnswer?.answerValue ?: ""
val conditionMet = when (condition.operator) {
"!=" -> answerValue != condition.value
"==" -> answerValue == condition.value
else -> true
}
if (conditionMet) break
else nextIndex++
} else {
break
}
withContext(Dispatchers.Main) {
updateButtonTexts()
}
if (nextIndex >= questionnaireEntries.size) {
setButtonsEnabled(emptyList())
val message = LanguageManager.getText(languageID, "questionnaires_finished")
Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
} else {
val nextFileName = questionnaireEntries[nextIndex].file
val nextButton = questionnaireFiles.entries.firstOrNull { it.value == nextFileName }?.key
setButtonsEnabled(listOfNotNull(nextButton))
// für jeden Fragebogen prüfen, ob er aktiv sein darf
val enabledButtons = mutableListOf<Button>()
for ((idx, entry) in questionnaireEntries.withIndex()) {
val button = dynamicButtons.getOrNull(idx) ?: continue
// falls bereits erledigt: nicht anklickbar
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) {
// 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 }
// key ableiten
val key = fileName.substringAfter("questionnaire_").substringAfter("_").removeSuffix(".json")
var buttonText = LanguageManager.getText(languageID, key)
@ -574,15 +692,12 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
// --- Füge diese Funktion in deine Klasse ein ---
private fun isDatabasePopulated(): Boolean {
return try {
// Wir prüfen, ob die Datenbank mindestens eine nicht-interne Tabelle enthält.
// Das ist robust gegenüber verschiedenen Tabellennamen.
val db = MyApp.database.openHelper.readableDatabase
val cursor = db.query(
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name != 'room_master_table'"
)
cursor.use { it.count > 0 }
} catch (e: Exception) {
// Falls etwas schiefgeht (z.B. DB noch nicht vorhanden), gilt: nicht vorhanden
false
}
}
@ -607,4 +722,4 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
button.alpha = if (isDatabaseAvailable) 1.0f else 0.5f
}
}
}
}

View File

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