added offline mode and improved coachcode system

This commit is contained in:
oxidiert
2025-09-29 13:02:00 +02:00
parent 851676f6c3
commit dcfa261c1c
3 changed files with 85 additions and 30 deletions

View File

@ -4,10 +4,10 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-09-29T09:46:59.707167200Z"> <DropdownSelection timestamp="2025-09-29T10:52:30.282144200Z">
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=HA218GZY" /> <DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\danie\.android\avd\Medium_Phone.avd" />
</handle> </handle>
</Target> </Target>
</DropdownSelection> </DropdownSelection>

View File

@ -27,29 +27,37 @@ class HandlerClientCoachCode(
this.question = question this.question = question
val clientCodeField = layout.findViewById<EditText>(R.id.client_code) val clientCodeField = layout.findViewById<EditText>(R.id.client_code)
val coachCodeField = layout.findViewById<EditText>(R.id.coach_code) val coachCodeField = layout.findViewById<EditText>(R.id.coach_code)
val questionTextView = layout.findViewById<TextView>(R.id.question) val questionTextView = layout.findViewById<TextView>(R.id.question)
val titleTextView = layout.findViewById<TextView>(R.id.textView) val titleTextView = layout.findViewById<TextView>(R.id.textView)
questionTextView.text = question.question?.let { LanguageManager.getText(languageID, it) } ?: "" questionTextView.text = question.question?.let { LanguageManager.getText(languageID, it) } ?: ""
setTextSizePercentOfScreenHeight(titleTextView, 0.03f) setTextSizePercentOfScreenHeight(titleTextView, 0.03f)
setTextSizePercentOfScreenHeight(questionTextView,0.03f) setTextSizePercentOfScreenHeight(questionTextView, 0.03f)
setTextSizePercentOfScreenHeight(clientCodeField, 0.025f) setTextSizePercentOfScreenHeight(clientCodeField, 0.025f)
setTextSizePercentOfScreenHeight(coachCodeField, 0.025f) setTextSizePercentOfScreenHeight(coachCodeField, 0.025f)
// *** WICHTIG: Nur den ERFOLGREICH GELADENEN Code verwenden *** // Client-Code: nur verwenden, wenn bereits geladen
val loadedClientCode = GlobalValues.LOADED_CLIENT_CODE val loadedClientCode = GlobalValues.LOADED_CLIENT_CODE
if (!loadedClientCode.isNullOrBlank()) { if (!loadedClientCode.isNullOrBlank()) {
clientCodeField.setText(loadedClientCode) clientCodeField.setText(loadedClientCode)
clientCodeField.isEnabled = false clientCodeField.isEnabled = false
} else { } else {
// Nichts ist geladen → Feld bleibt leer und editierbar
clientCodeField.setText("") clientCodeField.setText("")
clientCodeField.isEnabled = true clientCodeField.isEnabled = true
} }
coachCodeField.setText(answers["coach_code"] as? String ?: "") // === NEU: Coach-Code immer aus dem Login (TokenStore) setzen und sperren ===
val coachFromLogin = TokenStore.getUsername(layout.context)
if (!coachFromLogin.isNullOrBlank()) {
coachCodeField.setText(coachFromLogin)
lockCoachField(coachCodeField) // optisch & technisch gesperrt
} else {
// Falls (theoretisch) kein Login-Username vorhanden ist, verhalten wie bisher
coachCodeField.setText(answers["coach_code"] as? String ?: "")
coachCodeField.isEnabled = true
}
layout.findViewById<Button>(R.id.Qnext).setOnClickListener { layout.findViewById<Button>(R.id.Qnext).setOnClickListener {
onNextClicked(clientCodeField, coachCodeField) onNextClicked(clientCodeField, coachCodeField)
@ -67,7 +75,6 @@ class HandlerClientCoachCode(
} }
private fun onNextClicked(clientCodeField: EditText, coachCodeField: EditText) { private fun onNextClicked(clientCodeField: EditText, coachCodeField: EditText) {
// 1) Ohne vorher geladenen Client NICHT weiter
val loadedClientCode = GlobalValues.LOADED_CLIENT_CODE val loadedClientCode = GlobalValues.LOADED_CLIENT_CODE
if (!validate()) { if (!validate()) {
@ -77,7 +84,8 @@ class HandlerClientCoachCode(
} }
val clientCode = clientCodeField.text.toString() val clientCode = clientCodeField.text.toString()
val coachCode = coachCodeField.text.toString() // Erzwinge Coach-Code aus Login (falls vorhanden)
val coachCode = TokenStore.getUsername(layout.context) ?: coachCodeField.text.toString()
// Prüfen, ob die DB-Datei vor dem Zugriff existiert // Prüfen, ob die DB-Datei vor dem Zugriff existiert
val dbPath = layout.context.getDatabasePath("questionnaire_database") val dbPath = layout.context.getDatabasePath("questionnaire_database")
@ -87,7 +95,6 @@ class HandlerClientCoachCode(
val existingClient = MyApp.database.clientDao().getClientByCode(clientCode) val existingClient = MyApp.database.clientDao().getClientByCode(clientCode)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
// Wenn Feld editierbar war und Code bereits existiert → Hinweis
if (existingClient != null && clientCodeField.isEnabled) { if (existingClient != null && clientCodeField.isEnabled) {
val message = LanguageManager.getText(languageID, "client_code_exists") val message = LanguageManager.getText(languageID, "client_code_exists")
showToast(message) showToast(message)
@ -108,25 +115,44 @@ class HandlerClientCoachCode(
private fun onPreviousClicked(clientCodeField: EditText, coachCodeField: EditText) { private fun onPreviousClicked(clientCodeField: EditText, coachCodeField: EditText) {
val clientCode = clientCodeField.text.toString() val clientCode = clientCodeField.text.toString()
val coachCode = coachCodeField.text.toString() val coachCode = TokenStore.getUsername(layout.context) ?: coachCodeField.text.toString()
saveAnswers(clientCode, coachCode) saveAnswers(clientCode, coachCode)
goToPreviousQuestion() goToPreviousQuestion()
} }
override fun validate(): Boolean { override fun validate(): Boolean {
val clientCode = layout.findViewById<EditText>(R.id.client_code).text val clientCode = layout.findViewById<EditText>(R.id.client_code).text
val coachCode = layout.findViewById<EditText>(R.id.coach_code).text // Validierung nimmt den (ggf. gesperrten) Text passt
return clientCode.isNotBlank() && coachCode.isNotBlank() val coachText = layout.findViewById<EditText>(R.id.coach_code).text
return clientCode.isNotBlank() && coachText.isNotBlank()
} }
private fun saveAnswers(clientCode: String, coachCode: String) { private fun saveAnswers(clientCode: String, coachCode: String) {
// Optional: LAST_CLIENT_CODE kann gesetzt bleiben; maßgeblich ist LOADED_CLIENT_CODE
GlobalValues.LAST_CLIENT_CODE = clientCode GlobalValues.LAST_CLIENT_CODE = clientCode
answers["client_code"] = clientCode answers["client_code"] = clientCode
answers["coach_code"] = coachCode // Speichere garantierten Coach-Code aus Login bevorzugt
val loginCoach = TokenStore.getUsername(layout.context)
answers["coach_code"] = loginCoach ?: coachCode
} }
override fun saveAnswer() { override fun saveAnswer() {
// Not used // Not used
} }
// --- Helfer zum Sperren inkl. optischer Markierung (wie im Opening Screen) ---
private fun lockCoachField(field: EditText) {
field.isFocusable = false
field.isFocusableInTouchMode = false
field.isCursorVisible = false
field.keyListener = null
field.isLongClickable = false
field.isClickable = false
field.setBackgroundResource(R.drawable.bg_field_locked)
field.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_lock_24, 0)
field.compoundDrawablePadding = dp(8)
field.alpha = 0.95f
}
private fun dp(v: Int): Int =
(v * layout.resources.displayMetrics.density).toInt()
} }

View File

@ -13,6 +13,7 @@ import android.widget.ProgressBar
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import java.io.File
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@ -23,15 +24,33 @@ class MainActivity : AppCompatActivity() {
private var progress: ProgressBar? = null private var progress: ProgressBar? = null
// LIVE: Network-Callback (wird in onResume registriert, in onPause deregistriert) // LIVE: Network-Callback (optional für Statusleiste)
private var netCb: ConnectivityManager.NetworkCallback? = null private var netCb: ConnectivityManager.NetworkCallback? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// Login-Dialog -> Login -> Auto-Download -> OpeningScreen
// === Offline-Start ermöglichen ===
// Bedingung: gespeicherter User/Token UND lokale DB vorhanden -> direkt OpeningScreen
val hasCreds = !TokenStore.getUsername(this).isNullOrBlank() && !TokenStore.getToken(this).isNullOrBlank()
val hasDb = hasLocalDb()
if (hasCreds && hasDb) {
openingScreenHandler = HandlerOpeningScreen(this)
openingScreenHandler.init()
return
}
// Sonst: Login-Dialog -> Login -> DB (einmalig) laden -> OpeningScreen
showLoginThenDownload() showLoginThenDownload()
} }
/** Prüft, ob die lokale DB-Datei vorhanden ist. */
private fun hasLocalDb(): Boolean {
val dbFile = getDatabasePath("questionnaire_database")
return dbFile != null && dbFile.exists() && dbFile.length() > 0
}
/** Zeigt den Login-Dialog an, führt Login aus und lädt danach einmalig die DB. */
private fun showLoginThenDownload() { private fun showLoginThenDownload() {
val container = LinearLayout(this).apply { val container = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL orientation = LinearLayout.VERTICAL
@ -47,7 +66,8 @@ class MainActivity : AppCompatActivity() {
inputType = android.text.InputType.TYPE_CLASS_TEXT or inputType = android.text.InputType.TYPE_CLASS_TEXT or
android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD
} }
container.addView(etUser); container.addView(etPass) container.addView(etUser)
container.addView(etPass)
val dialog = AlertDialog.Builder(this) val dialog = AlertDialog.Builder(this)
.setTitle("Login erforderlich") .setTitle("Login erforderlich")
@ -67,18 +87,29 @@ class MainActivity : AppCompatActivity() {
username = user, username = user,
password = pass, password = pass,
onSuccess = { token -> onSuccess = { token ->
// Token & Login-Timestamp werden in TokenStore gespeichert (siehe LoginManager/TokenStore). // Nach erfolgreichem Login: einmalig komplette DB ziehen
DatabaseDownloader.downloadAndReplaceDatabase( DatabaseDownloader.downloadAndReplaceDatabase(
context = this, context = this,
token = token token = token
) { ok -> ) { ok ->
showBusy(false) showBusy(false)
if (!ok) {
Toast.makeText(this, "Download fehlgeschlagen", Toast.LENGTH_LONG).show() // Wenn Download fehlgeschlagen ist, aber evtl. schon eine DB lokal liegt,
// lassen wir den Nutzer trotzdem weiterarbeiten (Offline).
if (!ok && !hasLocalDb()) {
Toast.makeText(this, "Download fehlgeschlagen keine lokale Datenbank vorhanden", Toast.LENGTH_LONG).show()
// Zurück zum Login, damit man es erneut probieren kann
showLoginThenDownload()
return@downloadAndReplaceDatabase
} }
if (!ok) {
Toast.makeText(this, "Download fehlgeschlagen arbeite offline mit vorhandener DB", Toast.LENGTH_LONG).show()
}
// Opening-Screen starten
openingScreenHandler = HandlerOpeningScreen(this) openingScreenHandler = HandlerOpeningScreen(this)
openingScreenHandler.init() openingScreenHandler.init()
// Ersten Status sofort anzeigen
openingScreenHandler.refreshHeaderStatusLive() openingScreenHandler.refreshHeaderStatusLive()
} }
}, },
@ -111,11 +142,10 @@ class MainActivity : AppCompatActivity() {
private fun dp(v: Int): Int = (v * resources.displayMetrics.density).toInt() private fun dp(v: Int): Int = (v * resources.displayMetrics.density).toInt()
// --- LIVE NETZSTATUS --- // --- LIVE NETZSTATUS (optional, für deine Status-Leiste) ---
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
registerNetworkCallback() registerNetworkCallback()
// Falls die Startseite schon steht: Status jetzt gleich ziehen
if (::openingScreenHandler.isInitialized && !isInQuestionnaire) { if (::openingScreenHandler.isInitialized && !isInQuestionnaire) {
openingScreenHandler.refreshHeaderStatusLive() openingScreenHandler.refreshHeaderStatusLive()
} }
@ -127,7 +157,7 @@ class MainActivity : AppCompatActivity() {
} }
private fun registerNetworkCallback() { private fun registerNetworkCallback() {
if (netCb != null) return // schon aktiv if (netCb != null) return
val cm = getSystemService(ConnectivityManager::class.java) val cm = getSystemService(ConnectivityManager::class.java)
val req = NetworkRequest.Builder() val req = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
@ -147,7 +177,6 @@ class MainActivity : AppCompatActivity() {
} }
} }
} }
// Optional: auch auf „nicht validiert“ reagieren
override fun onCapabilitiesChanged(network: Network, caps: NetworkCapabilities) { override fun onCapabilitiesChanged(network: Network, caps: NetworkCapabilities) {
runOnUiThread { runOnUiThread {
if (::openingScreenHandler.isInitialized && !isInQuestionnaire) { if (::openingScreenHandler.isInitialized && !isInQuestionnaire) {