chore: remove obsolete Android app configuration files
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 44s
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:
68
mobile-app/composeApp/build.gradle.kts
Normal file
68
mobile-app/composeApp/build.gradle.kts
Normal 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)
|
||||
}
|
||||
25
mobile-app/composeApp/src/androidMain/AndroidManifest.xml
Normal file
25
mobile-app/composeApp/src/androidMain/AndroidManifest.xml
Normal 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>
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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()) }
|
||||
}
|
||||
@@ -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 ?: ""}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user