chore: remove obsolete Android app configuration files
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 44s

- Deleted build.gradle.kts, gradle.properties, and gradlew files as part of the cleanup process.
- Removed local.properties and various generated files from the .gradle directory to streamline the project structure.
- Cleared out unnecessary build artifacts and intermediate files to improve project maintainability.
This commit is contained in:
Torsten Schulz (local)
2026-04-21 15:15:21 +02:00
parent c8dedb10cc
commit 41bbf81958
4144 changed files with 4975 additions and 61401 deletions

View File

@@ -0,0 +1,68 @@
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidApplication)
alias(libs.plugins.jetbrainsCompose)
alias(libs.plugins.compose.compiler)
}
kotlin {
androidTarget {
compilations.all {
kotlinOptions {
jvmTarget = "1.8"
}
}
}
sourceSets {
commonMain.dependencies {
implementation(project(":shared"))
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material)
implementation(compose.ui)
implementation(compose.components.resources)
implementation(libs.voyager.navigator)
implementation(libs.voyager.screenmodel)
implementation(libs.voyager.koin)
implementation(libs.koin.core)
}
androidMain.dependencies {
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.core.ktx)
implementation(libs.koin.android)
}
}
}
android {
namespace = "de.tt_tagebuch.app"
compileSdk = libs.versions.android.compileSdk.get().toInt()
defaultConfig {
applicationId = "de.tt_tagebuch.app"
minSdk = libs.versions.android.minSdk.get().toInt()
targetSdk = libs.versions.android.targetSdk.get().toInt()
versionCode = 1
versionName = "1.0.0"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}
dependencies {
debugImplementation(libs.compose.ui.tooling)
}

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".MainApplication"
android:allowBackup="true"
android:icon="@android:mipmap/sym_def_app_icon"
android:label="Trainingstagebuch"
android:roundIcon="@android:mipmap/sym_def_app_icon"
android:supportsRtl="true"
android:theme="@android:style/Theme.Material.Light.NoActionBar">
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@android:style/Theme.Material.Light.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,14 @@
package de.tt_tagebuch.app
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
App()
}
}
}

View File

@@ -0,0 +1,18 @@
package de.tt_tagebuch.app
import android.app.Application
import de.tt_tagebuch.app.di.appModule
import de.tt_tagebuch.shared.di.initKoin
import org.koin.android.ext.koin.androidContext
class MainApplication : Application() {
override fun onCreate() {
super.onCreate()
initKoin(
baseUrl = "https://tt-tagebuch.de",
additionalModules = listOf(appModule)
) {
androidContext(this@MainApplication)
}
}
}

View File

@@ -0,0 +1,13 @@
package de.tt_tagebuch.app
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import cafe.adriel.voyager.navigator.Navigator
import de.tt_tagebuch.app.ui.LoginScreen
@Composable
fun App() {
MaterialTheme {
Navigator(LoginScreen())
}
}

View File

@@ -0,0 +1,16 @@
package de.tt_tagebuch.app.di
import de.tt_tagebuch.app.viewmodel.DiaryScreenModel
import de.tt_tagebuch.app.viewmodel.LoginScreenModel
import de.tt_tagebuch.app.viewmodel.MemberScreenModel
import de.tt_tagebuch.app.viewmodel.ParticipantScreenModel
import de.tt_tagebuch.app.viewmodel.MemberEditScreenModel
import org.koin.dsl.module
val appModule = module {
factory { LoginScreenModel(get()) }
factory { DiaryScreenModel(get()) }
factory { MemberScreenModel(get()) } // Updated
factory { ParticipantScreenModel(get()) }
factory { MemberEditScreenModel(get()) }
}

View File

@@ -0,0 +1,79 @@
package de.tt_tagebuch.app.ui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.koin.getScreenModel
import de.tt_tagebuch.app.viewmodel.DiaryScreenModel
import de.tt_tagebuch.app.viewmodel.DiaryState
class DiaryScreen(private val clubId: Int) : Screen {
@Composable
override fun Content() {
val viewModel = getScreenModel<DiaryScreenModel>()
val state by viewModel.state.collectAsState()
LaunchedEffect(clubId) {
viewModel.loadDiaryDates(clubId)
}
Scaffold(
topBar = {
TopAppBar(title = { Text("Trainingstagebuch") })
},
floatingActionButton = {
FloatingActionButton(onClick = { /* Add new entry */ }) {
Icon(Icons.Default.Add, contentDescription = "Add")
}
}
) { padding ->
Column(modifier = Modifier.padding(padding).fillMaxSize()) {
when (state) {
is DiaryState.Loading -> {
CircularProgressIndicator(modifier = Modifier.padding(16.dp))
}
is DiaryState.Success -> {
val dates = (state as DiaryState.Success).dates
LazyColumn {
items(dates) { date ->
DiaryDateItem(date) {
navigator?.push(ParticipantScreen(date.id, date.date))
}
}
}
}
is DiaryState.Error -> {
Text(
text = (state as DiaryState.Error).message,
color = MaterialTheme.colors.error,
modifier = Modifier.padding(16.dp)
)
}
}
}
}
}
@Composable
fun DiaryDateItem(date: de.tt_tagebuch.shared.models.DiaryDate, onClick: () -> Unit) {
Card(
modifier = Modifier.fillMaxWidth().padding(8.dp).clickable { onClick() },
elevation = 4.dp
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(date.date, style = MaterialTheme.typography.h6)
if (date.trainingStart != null) {
Text("${date.trainingStart} - ${date.trainingEnd ?: ""}")
}
}
}
}
}

View File

@@ -0,0 +1,41 @@
package de.tt_tagebuch.app.ui
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DateRange
import androidx.compose.material.icons.filled.Person
import androidx.compose.runtime.*
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator
class HomeScreen(private val clubId: Int) : Screen {
@Composable
override fun Content() {
var selectedItem by remember { mutableStateOf(0) }
val titles = listOf("Tagebuch", "Mitglieder")
val icons = listOf(Icons.Default.DateRange, Icons.Default.Person)
Scaffold(
bottomBar = {
BottomNavigation {
titles.forEachIndexed { index, title ->
BottomNavigationItem(
icon = { Icon(icons[index], contentDescription = title) },
label = { Text(title) },
selected = selectedItem == index,
onClick = { selectedItem = index }
)
}
}
}
) { padding ->
Box(modifier = androidx.compose.foundation.layout.padding(padding)) {
when (selectedItem) {
0 -> DiaryScreen(clubId).Content()
1 -> MemberScreen(clubId).Content()
}
}
}
}
}

View File

@@ -0,0 +1,79 @@
package de.tt_tagebuch.app.ui
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.koin.getScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import de.tt_tagebuch.app.viewmodel.LoginScreenModel
import de.tt_tagebuch.app.viewmodel.LoginState
import de.tt_tagebuch.app.ui.HomeScreen
class LoginScreen : Screen {
@Composable
override fun Content() {
val navigator = LocalNavigator.current
val viewModel = getScreenModel<LoginScreenModel>()
val state by viewModel.state.collectAsState()
var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
Column(
modifier = Modifier.fillMaxSize().padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Trainingstagebuch", style = MaterialTheme.typography.h4)
Spacer(modifier = Modifier.height(32.dp))
TextField(
value = email,
onValueChange = { email = it },
label = { Text("Email") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
TextField(
value = password,
onValueChange = { password = it },
label = { Text("Passwort") },
visualTransformation = PasswordVisualTransformation(),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(32.dp))
Button(
onClick = { viewModel.login(email, password) },
enabled = state !is LoginState.Loading,
modifier = Modifier.fillMaxWidth()
) {
if (state is LoginState.Loading) {
CircularProgressIndicator(color = MaterialTheme.colors.onPrimary)
} else {
Text("Login")
}
}
if (state is LoginState.Error) {
Text(
text = (state as LoginState.Error).message,
color = MaterialTheme.colors.error,
modifier = Modifier.padding(top = 16.dp)
)
}
LaunchedEffect(state) {
if (state is LoginState.Success) {
navigator?.push(HomeScreen(1)) // Using clubId 1 for now
}
}
}
}
}

View File

@@ -0,0 +1,144 @@
package de.tt_tagebuch.app.ui
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Email
import androidx.compose.material.icons.filled.Phone
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import de.tt_tagebuch.shared.models.Member
class MemberDetailScreen(private val member: Member) : Screen {
@Composable
override fun Content() {
val navigator = LocalNavigator.current
Scaffold(
topBar = {
TopAppBar(
title = { Text("${member.firstName} ${member.lastName}") },
navigationIcon = {
IconButton(onClick = { navigator?.pop() }) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
}
},
actions = {
IconButton(onClick = {
navigator?.push(MemberEditScreen(member.clubId, member))
}) {
Icon(androidx.compose.material.icons.filled.Edit, contentDescription = "Edit")
}
}
)
}
) { padding ->
Column(
modifier = Modifier
.padding(padding)
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(16.dp)
) {
// Header with Status Badges
Row(verticalAlignment = Alignment.CenterVertically) {
if (member.active) {
StatusBadge("Aktiv", Color(0xFF4CAF50))
} else {
StatusBadge("Inaktiv", Color.Red)
}
if (member.testMembership) {
Spacer(Modifier.width(8.dp))
StatusBadge("Test", Color.Orange)
}
}
Spacer(Modifier.height(16.dp))
// TTR Info
Card(elevation = 2.dp, modifier = Modifier.fillMaxWidth()) {
Row(modifier = Modifier.padding(16.dp), horizontalArrangement = Arrangement.SpaceBetween) {
Column {
Text("TTR", style = MaterialTheme.typography.caption)
Text("${member.ttr ?: "-"}", style = MaterialTheme.typography.h5)
}
Column {
Text("QTTR", style = MaterialTheme.typography.caption)
Text("${member.qttr ?: "-"}", style = MaterialTheme.typography.h5)
}
Column {
Text("Alter", style = MaterialTheme.typography.caption)
Text(member.birthDate ?: "-", style = MaterialTheme.typography.h5)
}
}
}
Spacer(Modifier.height(24.dp))
// Contact Details
Text("Kontaktdaten", style = MaterialTheme.typography.h6)
member.contacts.forEach { contact ->
ContactRow(contact)
}
if (member.contacts.isEmpty()) {
Text("Keine Kontaktdaten hinterlegt", style = MaterialTheme.typography.body2, color = Color.Gray)
}
Spacer(Modifier.height(24.dp))
// Attendance Info
Text("Statistik", style = MaterialTheme.typography.h6)
Text("Trainingsteilnahmen: ${member.trainingParticipations}", style = MaterialTheme.typography.body1)
Text("Zuletzt im Training: ${member.lastTraining ?: "Nie"}", style = MaterialTheme.typography.body1)
}
}
}
@Composable
fun StatusBadge(text: String, color: Color) {
Surface(
color = color.copy(alpha = 0.1f),
shape = MaterialTheme.shapes.small,
border = androidx.compose.foundation.BorderStroke(1.dp, color)
) {
Text(
text = text,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp),
style = MaterialTheme.typography.caption,
color = color
)
}
}
@Composable
fun ContactRow(contact: de.tt_tagebuch.shared.models.MemberContact) {
Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
if (contact.type == "phone") Icons.Default.Phone else Icons.Default.Email,
contentDescription = null,
tint = MaterialTheme.colors.primary,
modifier = Modifier.size(20.dp)
)
Spacer(Modifier.width(16.dp))
Column {
Text(contact.value, style = MaterialTheme.typography.body1)
if (contact.isParent) {
Text("Elternteil${if (contact.parentName != null) ": ${contact.parentName}" else ""}",
style = MaterialTheme.typography.caption, color = Color.Gray)
}
}
}
}
}

View File

@@ -0,0 +1,159 @@
package de.tt_tagebuch.app.ui
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Delete
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.koin.getScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import de.tt_tagebuch.app.viewmodel.MemberEditScreenModel
import de.tt_tagebuch.app.viewmodel.MemberEditState
import de.tt_tagebuch.shared.models.Member
import de.tt_tagebuch.shared.models.MemberContact
class MemberEditScreen(private val clubId: Int, private val member: Member? = null) : Screen {
@Composable
override fun Content() {
val navigator = LocalNavigator.current
val viewModel = getScreenModel<MemberEditScreenModel>()
val state by viewModel.state.collectAsState()
var firstName by remember { mutableStateOf(member?.firstName ?: "") }
var lastName by remember { mutableStateOf(member?.lastName ?: "") }
var birthDate by remember { mutableStateOf(member?.birthDate ?: "") }
var gender by remember { mutableStateOf(member?.gender ?: "unknown") }
var active by remember { mutableStateOf(member?.active ?: true) }
var testMembership by remember { mutableStateOf(member?.testMembership ?: true) }
var contacts by remember { mutableStateOf(member?.contacts?.toMutableList() ?: mutableListOf()) }
Scaffold(
topBar = {
TopAppBar(
title = { Text(if (member == null) "Neues Mitglied" else "Mitglied bearbeiten") },
navigationIcon = {
IconButton(onClick = { navigator?.pop() }) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
}
},
actions = {
TextButton(onClick = {
val newMember = Member(
id = member?.id ?: 0,
firstName = firstName,
lastName = lastName,
clubId = clubId,
active = active,
birthDate = birthDate,
gender = gender,
testMembership = testMembership,
contacts = contacts
)
viewModel.saveMember(clubId, newMember)
}, enabled = state !is MemberEditState.Saving) {
Text("Speichern", color = Color.White)
}
}
)
}
) { padding ->
if (state is MemberEditState.Saving) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
}
Column(
modifier = Modifier
.padding(padding)
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(16.dp)
) {
Text("Stammdaten", style = MaterialTheme.typography.h6)
TextField(
value = firstName,
onValueChange = { firstName = it },
label = { Text("Vorname") },
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.height(8.dp))
TextField(
value = lastName,
onValueChange = { lastName = it },
label = { Text("Nachname") },
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.height(8.dp))
TextField(
value = birthDate,
onValueChange = { birthDate = it },
label = { Text("Geburtsdatum (YYYY-MM-DD)") },
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.height(16.dp))
Text("Status", style = MaterialTheme.typography.h6)
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = active, onCheckedChange = { active = it })
Text("Aktiv")
Spacer(Modifier.width(16.dp))
Checkbox(checked = testMembership, onCheckedChange = { testMembership = it })
Text("Probetraining")
}
Spacer(Modifier.height(24.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text("Kontakte", style = MaterialTheme.typography.h6)
IconButton(onClick = {
contacts = (contacts + MemberContact(type = "phone", value = "")).toMutableList()
}) {
Icon(Icons.Default.Add, contentDescription = "Add Contact")
}
}
contacts.forEachIndexed { index, contact ->
Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
TextField(
value = contact.value,
onValueChange = { newValue ->
val newList = contacts.toMutableList()
newList[index] = contact.copy(value = newValue)
contacts = newList
},
label = { Text(if (contact.type == "phone") "Telefon" else "Email") },
modifier = Modifier.weight(1f)
)
IconButton(onClick = {
val newList = contacts.toMutableList()
newList.removeAt(index)
contacts = newList
}) {
Icon(Icons.Default.Delete, contentDescription = "Remove")
}
}
}
}
LaunchedEffect(state) {
if (state is MemberEditState.Success) {
navigator?.pop()
}
}
}
}
}

View File

@@ -0,0 +1,80 @@
package de.tt_tagebuch.app.ui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.koin.getScreenModel
import de.tt_tagebuch.app.viewmodel.MemberScreenModel
import de.tt_tagebuch.app.viewmodel.MemberState
import cafe.adriel.voyager.navigator.LocalNavigator
class MemberScreen(private val clubId: Int) : Screen {
@Composable
override fun Content() {
val navigator = LocalNavigator.current
val viewModel = getScreenModel<MemberScreenModel>()
val state by viewModel.state.collectAsState()
LaunchedEffect(clubId) {
viewModel.loadMembers(clubId)
}
Scaffold(
topBar = {
TopAppBar(title = { Text("Mitglieder") })
},
floatingActionButton = {
FloatingActionButton(onClick = {
navigator?.push(MemberEditScreen(clubId))
}) {
Icon(Icons.Default.Add, contentDescription = "Add Member")
}
}
) { padding ->
Column(modifier = Modifier.padding(padding).fillMaxSize()) {
when (state) {
is MemberState.Loading -> {
CircularProgressIndicator(modifier = Modifier.padding(16.dp))
}
is MemberState.Success -> {
val members = (state as MemberState.Success).members
LazyColumn {
items(members) { member ->
MemberItem(member) {
navigator?.push(MemberDetailScreen(member))
}
}
}
}
is MemberState.Error -> {
Text(
text = (state as MemberState.Error).message,
color = MaterialTheme.colors.error,
modifier = Modifier.padding(16.dp)
)
}
}
}
}
}
@Composable
fun MemberItem(member: de.tt_tagebuch.shared.models.Member, onClick: () -> Unit) {
ListItem(
modifier = Modifier.clickable { onClick() },
text = { Text("${member.firstName} ${member.lastName}") },
secondaryText = { if (!member.active) Text("Inaktiv", color = MaterialTheme.colors.error) }
)
Divider()
}
}

View File

@@ -0,0 +1,117 @@
package de.tt_tagebuch.app.ui
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.koin.getScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import de.tt_tagebuch.app.viewmodel.ParticipantScreenModel
import de.tt_tagebuch.app.viewmodel.ParticipantState
class ParticipantScreen(private val dateId: Int, private val dateStr: String) : Screen {
@Composable
override fun Content() {
val navigator = LocalNavigator.current
val viewModel = getScreenModel<ParticipantScreenModel>()
val state by viewModel.state.collectAsState()
LaunchedEffect(dateId) {
viewModel.loadParticipants(dateId)
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("Teilnehmer - $dateStr") },
navigationIcon = {
IconButton(onClick = { navigator?.pop() }) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
}
}
)
}
) { padding ->
Column(modifier = Modifier.padding(padding).fillMaxSize()) {
when (state) {
is ParticipantState.Loading -> {
CircularProgressIndicator(modifier = Modifier.padding(16.dp))
}
is ParticipantState.Success -> {
val participants = (state as ParticipantState.Success).participants
LazyColumn {
items(participants) { participant ->
ParticipantItem(participant) { newStatus ->
viewModel.updateStatus(dateId, participant.memberId, newStatus)
}
}
}
}
is ParticipantState.Error -> {
Text(
text = (state as ParticipantState.Error).message,
color = MaterialTheme.colors.error,
modifier = Modifier.padding(16.dp)
)
}
}
}
}
}
@Composable
fun ParticipantItem(
participant: de.tt_tagebuch.shared.models.Participant,
onStatusChange: (String) -> Unit
) {
val member = participant.member
val name = if (member != null) "${member.firstName} ${member.lastName}" else "Unbekannt"
Card(modifier = Modifier.fillMaxWidth().padding(8.dp), elevation = 2.dp) {
Column(modifier = Modifier.padding(16.dp)) {
Text(name, style = MaterialTheme.typography.h6)
Spacer(Modifier.height(8.dp))
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
StatusToggle("Anwesend", participant.attendanceStatus == "present", Color(0xFF4CAF50)) {
onStatusChange("present")
}
StatusToggle("Entschuldigt", participant.attendanceStatus == "excused", Color(0xFF2196F3)) {
onStatusChange("excused")
}
StatusToggle("Abgesagt", participant.attendanceStatus == "cancelled", Color.Red) {
onStatusChange("cancelled")
}
}
}
}
}
@Composable
fun StatusToggle(label: String, isSelected: Boolean, color: Color, onClick: () -> Unit) {
val backgroundColor = if (isSelected) color else color.copy(alpha = 0.1f)
val contentColor = if (isSelected) Color.White else color
Button(
onClick = onClick,
colors = ButtonDefaults.buttonColors(
backgroundColor = backgroundColor,
contentColor = contentColor
),
elevation = ButtonDefaults.elevation(0.dp),
shape = MaterialTheme.shapes.small,
modifier = Modifier.height(32.dp).padding(horizontal = 4.dp),
contentPadding = PaddingValues(horizontal = 8.dp)
) {
Text(label, style = MaterialTheme.typography.caption)
}
}
}

View File

@@ -0,0 +1,44 @@
package de.tt_tagebuch.app.viewmodel
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import de.tt_tagebuch.shared.models.DiaryDate
import de.tt_tagebuch.shared.repository.DiaryRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class DiaryScreenModel(private val diaryRepository: DiaryRepository) : ScreenModel {
private val _state = MutableStateFlow<DiaryState>(DiaryState.Loading)
val state = _state.asStateFlow()
fun loadDiaryDates(clubId: Int) {
screenModelScope.launch {
try {
val dates = diaryRepository.getDiaryDates(clubId)
_state.value = DiaryState.Success(dates)
diaryRepository.connectSocket(clubId)
// Observe socket events
diaryRepository.observeEvents().collect { (event, data) ->
// Handle real-time updates here
// For now, just reload to keep it simple
loadDiaryDates(clubId)
}
} catch (e: Exception) {
_state.value = DiaryState.Error(e.message ?: "Failed to load diary")
}
}
}
override fun onDispose() {
diaryRepository.disconnectSocket()
super.onDispose()
}
}
sealed class DiaryState {
object Loading : DiaryState()
data class Success(val dates: List<DiaryDate>) : DiaryState()
data class Error(val message: String) : DiaryState()
}

View File

@@ -0,0 +1,36 @@
package de.tt_tagebuch.app.viewmodel
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import de.tt_tagebuch.shared.repository.AuthRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class LoginScreenModel(private val authRepository: AuthRepository) : ScreenModel {
private val _state = MutableStateFlow<LoginState>(LoginState.Idle)
val state = _state.asStateFlow()
fun login(email: String, password: String) {
screenModelScope.launch {
_state.value = LoginState.Loading
try {
val response = authRepository.login(email, password)
if (response.success) {
_state.value = LoginState.Success
} else {
_state.value = LoginState.Error(response.error ?: "Unknown error")
}
} catch (e: Exception) {
_state.value = LoginState.Error(e.message ?: "Network error")
}
}
}
}
sealed class LoginState {
object Idle : LoginState()
object Loading : LoginState()
object Success : LoginState()
data class Error(val message: String) : LoginState()
}

View File

@@ -0,0 +1,37 @@
package de.tt_tagebuch.app.viewmodel
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import de.tt_tagebuch.shared.models.Member
import de.tt_tagebuch.shared.repository.MemberRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class MemberEditScreenModel(private val memberRepository: MemberRepository) : ScreenModel {
private val _state = MutableStateFlow<MemberEditState>(MemberEditState.Idle)
val state = _state.asStateFlow()
fun saveMember(clubId: Int, member: Member) {
screenModelScope.launch {
_state.value = MemberEditState.Saving
try {
val success = memberRepository.saveMember(clubId, member)
if (success) {
_state.value = MemberEditState.Success
} else {
_state.value = MemberEditState.Error("Fehler beim Speichern")
}
} catch (e: Exception) {
_state.value = MemberEditState.Error(e.message ?: "Netzwerkfehler")
}
}
}
}
sealed class MemberEditState {
object Idle : MemberEditState()
object Saving : MemberEditState()
object Success : MemberEditState()
data class Error(val message: String) : MemberEditState()
}

View File

@@ -0,0 +1,34 @@
package de.tt_tagebuch.app.viewmodel
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import de.tt_tagebuch.shared.models.Member
import de.tt_tagebuch.shared.repository.DiaryRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class MemberScreenModel(private val diaryRepository: DiaryRepository) : ScreenModel {
// Note: Reusing DiaryRepository as it likely handles club-related data,
// but in a larger app we would have a MemberRepository.
private val _state = MutableStateFlow<MemberState>(MemberState.Loading)
val state = _state.asStateFlow()
fun loadMembers(clubId: Int) {
screenModelScope.launch {
try {
val members = diaryRepository.getMembers(clubId)
_state.value = MemberState.Success(members)
} catch (e: Exception) {
_state.value = MemberState.Error(e.message ?: "Failed to load members")
}
}
}
}
sealed class MemberState {
object Loading : MemberState()
data class Success(val members: List<Member>) : MemberState()
data class Error(val message: String) : MemberState()
}

View File

@@ -0,0 +1,46 @@
package de.tt_tagebuch.app.viewmodel
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import de.tt_tagebuch.shared.models.Participant
import de.tt_tagebuch.shared.repository.DiaryRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class ParticipantScreenModel(private val diaryRepository: DiaryRepository) : ScreenModel {
private val _state = MutableStateFlow<ParticipantState>(ParticipantState.Loading)
val state = _state.asStateFlow()
fun loadParticipants(dateId: Int) {
screenModelScope.launch {
_state.value = ParticipantState.Loading
try {
val participants = diaryRepository.getParticipants(dateId)
_state.value = ParticipantState.Success(participants)
} catch (e: Exception) {
_state.value = ParticipantState.Error(e.message ?: "Failed to load participants")
}
}
}
fun updateStatus(dateId: Int, memberId: Int, status: String) {
screenModelScope.launch {
try {
val success = diaryRepository.updateParticipantStatus(dateId, memberId, status)
if (success) {
// Refresh list after update
loadParticipants(dateId)
}
} catch (e: Exception) {
// Handle error
}
}
}
}
sealed class ParticipantState {
object Loading : ParticipantState()
data class Success(val participants: List<Participant>) : ParticipantState()
data class Error(val message: String) : ParticipantState()
}