diff --git a/composeApp/src/commonMain/kotlin/ch/parano/myice/ui/screens/EventDetailScreen.kt b/composeApp/src/commonMain/kotlin/ch/parano/myice/ui/screens/EventDetailScreen.kt new file mode 100644 index 0000000..a6da961 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ch/parano/myice/ui/screens/EventDetailScreen.kt @@ -0,0 +1,166 @@ +package ch.parano.myice.ui.screens + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import ch.parano.myice.ui.components.LoadingIndicator +import ch.parano.myice.viewmodel.EventDetailViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun EventDetailScreen( + gameId: String, + account: String, + eventTitle: String, + viewModel: EventDetailViewModel, + modifier: Modifier = Modifier, +) { + LaunchedEffect(gameId, account) { + viewModel.loadEventDetail(gameId, account) + } + + val state by viewModel.state.collectAsState() + + Scaffold( + topBar = { + TopAppBar(title = { Text(eventTitle) }) + }, + modifier = modifier, + ) { padding -> + when { + state.isLoading -> { + LoadingIndicator(modifier = Modifier.padding(padding)) + } + state.error != null -> { + Column( + modifier = Modifier.fillMaxSize().padding(padding), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + text = state.error!!, + color = MaterialTheme.colorScheme.error, + ) + Spacer(modifier = Modifier.height(8.dp)) + TextButton(onClick = { viewModel.loadEventDetail(gameId, account) }) { + Text("Reessayer") + } + } + } + state.eventDetail != null -> { + val detail = state.eventDetail!! + val hasPlayers = detail.convocation.available.isNotEmpty() + val hasStaff = detail.convocation.staff.isNotEmpty() + + LazyColumn( + modifier = Modifier.fillMaxSize().padding(padding), + contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + item { + Card(modifier = Modifier.fillMaxWidth()) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text("Type: ${detail.type}") + Text("Lieu: ${detail.place}") + Text("Heure: ${detail.timeStart} - ${detail.timeEnd}") + } + } + } + + if (!hasPlayers && !hasStaff) { + item { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFFFC107), + ), + ) { + Text( + text = "Aucun joueur ni personnel convoque", + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(16.dp), + ) + } + } + } + + if (hasPlayers) { + item { + Text( + text = "Joueurs (${detail.convocation.available.size})", + style = MaterialTheme.typography.titleLarge, + ) + } + items(detail.convocation.available) { player -> + val position = player.position ?: "N/A" + val number = player.number ?: "N/A" + Card(modifier = Modifier.fillMaxWidth()) { + Column( + modifier = Modifier.padding(16.dp), + ) { + Text("[$position] #$number - ${player.fname} ${player.lname}") + Text( + text = "DOB: ${player.dob}", + style = MaterialTheme.typography.bodySmall, + ) + } + } + } + } + + if (hasStaff) { + item { + Text( + text = "Personnel", + style = MaterialTheme.typography.titleLarge, + ) + } + items(detail.convocation.staff) { staff -> + Card(modifier = Modifier.fillMaxWidth()) { + Text( + text = "${staff.role}: ${staff.fname} ${staff.lname}", + modifier = Modifier.padding(16.dp), + ) + } + } + } + } + } + else -> { + Column( + modifier = Modifier.fillMaxSize().padding(padding), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text("Aucune donnee disponible") + } + } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/ch/parano/myice/ui/screens/LoginScreen.kt b/composeApp/src/commonMain/kotlin/ch/parano/myice/ui/screens/LoginScreen.kt new file mode 100644 index 0000000..cfd09be --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ch/parano/myice/ui/screens/LoginScreen.kt @@ -0,0 +1,72 @@ +package ch.parano.myice.ui.screens + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import ch.parano.myice.viewmodel.AuthViewModel +import ch.parano.myice.viewmodel.AuthState + +@Composable +fun LoginScreen( + viewModel: AuthViewModel, + modifier: Modifier = Modifier, +) { + val state by viewModel.state.collectAsState() + val snackbarHostState = remember { SnackbarHostState() } + + LaunchedEffect(state) { + if (state is AuthState.Unauthenticated) { + snackbarHostState.showSnackbar("Erreur de connexion") + } + } + + Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, + modifier = modifier, + ) { padding -> + Column( + modifier = Modifier.fillMaxSize().padding(padding), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + text = "MyIce", + style = MaterialTheme.typography.headlineLarge, + fontWeight = FontWeight.Bold, + ) + Text( + text = "Games", + style = MaterialTheme.typography.titleMedium, + ) + Spacer(modifier = Modifier.height(32.dp)) + if (state is AuthState.Loading) { + CircularProgressIndicator() + } else { + Button( + onClick = { viewModel.login() }, + ) { + Text("Se connecter avec Infomaniak") + } + } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/ch/parano/myice/ui/screens/ScheduleScreen.kt b/composeApp/src/commonMain/kotlin/ch/parano/myice/ui/screens/ScheduleScreen.kt new file mode 100644 index 0000000..07d0ee3 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ch/parano/myice/ui/screens/ScheduleScreen.kt @@ -0,0 +1,200 @@ +package ch.parano.myice.ui.screens + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import ch.parano.myice.filter.TypeFilter +import ch.parano.myice.ui.components.EventCard +import ch.parano.myice.ui.components.FilterDropdown +import ch.parano.myice.ui.components.LoadingIndicator +import ch.parano.myice.viewmodel.AuthViewModel +import ch.parano.myice.viewmodel.ScheduleViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ScheduleScreen( + scheduleViewModel: ScheduleViewModel, + authViewModel: AuthViewModel, + onEventClick: (gameId: String, account: String, eventTitle: String) -> Unit, + modifier: Modifier = Modifier, +) { + val scheduleState by scheduleViewModel.state.collectAsState() + val authState by authViewModel.state.collectAsState() + + Scaffold( + topBar = { + TopAppBar( + title = { Text("MyIce") }, + actions = { + val email = (authState as? ch.parano.myice.viewmodel.AuthState.Authenticated)?.email + if (email != null) { + Text( + text = email, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(horizontal = 8.dp), + ) + } + TextButton(onClick = { scheduleViewModel.refreshSchedule() }) { + Text("Refresh") + } + TextButton(onClick = { + scheduleViewModel.clearCache() + authViewModel.logout() + }) { + Text("Logout") + } + }, + ) + }, + modifier = modifier, + ) { padding -> + Column( + modifier = Modifier.fillMaxSize().padding(padding), + ) { + Card( + modifier = Modifier.fillMaxWidth().padding(16.dp), + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + FilterDropdown( + label = "Compte", + selectedValue = scheduleState.selectedAccount, + options = scheduleState.accounts.map { it.name }, + onSelect = { it?.let { acc -> scheduleViewModel.setAccount(acc) } }, + ) + FilterDropdown( + label = "Age", + selectedValue = scheduleState.selectedAgegroup, + options = scheduleState.agegroups, + onSelect = { scheduleViewModel.setAgegroup(it) }, + ) + FilterDropdown( + label = "Sous-groupe", + selectedValue = scheduleState.selectedSubgroup, + options = scheduleState.subgroups, + onSelect = { scheduleViewModel.setSubgroup(it) }, + ) + FilterDropdown( + label = "Type", + selectedValue = typeFilterLabel(scheduleState.typeFilter), + options = listOf("Tous", "Matchs", "Entrainements"), + onSelect = { label -> + val filter = when (label) { + "Tous" -> TypeFilter.ALL + "Matchs" -> TypeFilter.GAMES + "Entrainements" -> TypeFilter.PRACTICES + else -> TypeFilter.DEFAULT + } + scheduleViewModel.setTypeFilter(filter) + }, + ) + } + } + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + when { + scheduleState.isLoading && scheduleState.events.isEmpty() -> { + LoadingIndicator() + } + scheduleState.error != null && scheduleState.events.isEmpty() -> { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = scheduleState.error!!, + color = MaterialTheme.colorScheme.error, + ) + Spacer(modifier = Modifier.height(8.dp)) + TextButton(onClick = { scheduleViewModel.refreshSchedule() }) { + Text("Reessayer") + } + } + } + scheduleState.filteredEvents.isEmpty() -> { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text("Aucun evenement disponible") + if (scheduleState.events.isEmpty()) { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Appuyez sur le bouton de rafraichissement pour charger les evenements", + style = MaterialTheme.typography.bodySmall, + color = Color.Gray, + ) + } + } + } + else -> { + LazyColumn( + modifier = Modifier.fillMaxSize(), + ) { + items(scheduleState.filteredEvents) { event -> + EventCard( + event = event, + onClick = { + onEventClick( + event.idEvent, + scheduleState.selectedAccount!!, + event.title, + ) + }, + ) + } + } + } + } + if (scheduleState.isRefreshing) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Card { + Column( + modifier = Modifier.padding(32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + CircularProgressIndicator() + Spacer(modifier = Modifier.height(16.dp)) + Text("Chargement...", fontWeight = FontWeight.Medium) + } + } + } + } + } + } + } +} + +private fun typeFilterLabel(filter: TypeFilter): String = when (filter) { + TypeFilter.ALL -> "Tous" + TypeFilter.GAMES -> "Matchs" + TypeFilter.PRACTICES -> "Entrainements" +}