diff --git a/.eslintignore b/.eslintignore index 2daeae55..e8971afa 100644 --- a/.eslintignore +++ b/.eslintignore @@ -13,3 +13,5 @@ package-lock.json yarn.lock taxidriver-android-app + +/src/lib/motis diff --git a/.gitignore b/.gitignore index db3896b6..02febdb2 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ booking-generator/test .vscode /driver-app/.gradle/ /driver-app/.idea +/driver-app/local.properties diff --git a/.prettierignore b/.prettierignore index d291286a..afc65a11 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,5 +5,5 @@ yarn.lock src/lib/components/* .github/* migrations/* - +src/lib/motis taxidriver-android-app diff --git a/docker-compose.yml b/docker-compose.yml index 449c5ce3..7af8d9db 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: '3' services: pg: - image: postgis/postgis + image: postgis/postgis:16-3.4 restart: always container_name: pg ports: diff --git a/driver-app/app/src/main/java/de/motis/prima/Home.kt b/driver-app/app/src/main/java/de/motis/prima/Home.kt index bed66d28..28efd2db 100644 --- a/driver-app/app/src/main/java/de/motis/prima/Home.kt +++ b/driver-app/app/src/main/java/de/motis/prima/Home.kt @@ -2,6 +2,7 @@ package de.motis.prima import android.util.Log import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -11,16 +12,29 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.ExitToApp +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.Button +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Alignment +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.ViewModel @@ -50,9 +64,11 @@ class HomeViewModel : ViewModel() { } } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun Home( navController: NavController, + vehiclesViewModel: VehiclesViewModel, viewModel: HomeViewModel = androidx.lifecycle.viewmodel.compose.viewModel() ) { LaunchedEffect(key1 = viewModel) { @@ -66,48 +82,111 @@ fun Home( } } + var dropdownExpanded by remember { + mutableStateOf(false) + } + Scaffold( - modifier = Modifier - .fillMaxSize() - ) { contentPadding -> + topBar = { + CenterAlignedTopAppBar( + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary, + ), + title = { + Text( + "Home", + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + navigationIcon = { + IconButton(onClick = { navController.navigate("home") }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Localized description" + ) + } + }, + actions = { + IconButton(onClick = { dropdownExpanded = !dropdownExpanded }) { + Icon(Icons.Filled.MoreVert, contentDescription = "More Options") + } + DropdownMenu( + expanded = dropdownExpanded, + onDismissRequest = { dropdownExpanded = false } + ) { + DropdownMenuItem( + onClick = { + dropdownExpanded = false + navController.navigate("vehicles") + + }, + text = {Text("Fahrzeug wechseln")} + ) + DropdownMenuItem( + onClick = { + viewModel.logout() + dropdownExpanded = false + + }, + text = { Text("Logout") } + ) + } + } + ) + + } + ) { innerPadding -> Column( modifier = Modifier .fillMaxSize() - .padding(contentPadding) + .padding(innerPadding) ) { + Spacer(modifier = Modifier.height(42.dp)) Row( modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.End - ) { - TextButton(onClick = { viewModel.logout() }) { - Icon( - Icons.AutoMirrored.Outlined.ExitToApp, contentDescription = null - ) - } - } - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center ) { - Button( - modifier = Modifier.width(300.dp), - onClick = { - navController.navigate("vehicles") {} + if (vehiclesViewModel.selectedVehicleId == 0) { + Button( + modifier = Modifier.width(300.dp), + onClick = { + navController.navigate("vehicles") {} + } + ) { + Text( + text = "Fahrzeug auswählen", + fontSize = 24.sp, + textAlign = TextAlign.Center + ) + } + } else { + var licensePlate = "" + try { + val vehicle = vehiclesViewModel.vehicles.value.filter { + v -> v.id == vehiclesViewModel.selectedVehicleId + }[0] + licensePlate = vehicle.license_plate + } catch (e: Exception) { + licensePlate = "Kein Fahrzeug ausgewählt" + } + + Box() { + Text( + text = licensePlate, + fontWeight = FontWeight.Bold, + fontSize = 24.sp, + textAlign = TextAlign.Center + ) } - ) { - Text( - text = "Fahrzeug auswählen", fontSize = 24.sp - ) } } - Spacer(modifier = Modifier.height(20.dp)) + Spacer(modifier = Modifier.height(36.dp)) Row( modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center ) { Button( @@ -117,7 +196,9 @@ fun Home( } ) { Text( - text = "Aufträge", fontSize = 24.sp + text = "Aufträge", + fontSize = 24.sp, + textAlign = TextAlign.Center ) } } diff --git a/driver-app/app/src/main/java/de/motis/prima/Login.kt b/driver-app/app/src/main/java/de/motis/prima/Login.kt index 315ca3d2..62bdde9e 100644 --- a/driver-app/app/src/main/java/de/motis/prima/Login.kt +++ b/driver-app/app/src/main/java/de/motis/prima/Login.kt @@ -76,6 +76,7 @@ class LoginViewModel : ViewModel() { @Composable fun Login( navController: NavController, + vehiclesViewModel: VehiclesViewModel, viewModel: LoginViewModel = androidx.lifecycle.viewmodel.compose.viewModel() ) { val snackbarHostState = remember { SnackbarHostState() } @@ -92,14 +93,15 @@ fun Login( LaunchedEffect(key1 = viewModel) { // Catching successful login event and navigation to the next screen launch { + // vehiclesViewModel.selectedVehicleId = 0 viewModel.navigationEvent.collect { shouldNavigate -> Log.d("Navigation event", "Navigation triggered.") if (shouldNavigate) { Log.d("Navigation event", "Navigating to vehicle selection.") - navController.navigate("home") { - popUpTo("login") { - inclusive = true - } + if (vehiclesViewModel.selectedVehicleId == 0) { + navController.navigate("vehicles") {} + } else { + navController.navigate("tours") {} } } } diff --git a/driver-app/app/src/main/java/de/motis/prima/Nav.kt b/driver-app/app/src/main/java/de/motis/prima/Nav.kt index 814dabe3..6a010718 100644 --- a/driver-app/app/src/main/java/de/motis/prima/Nav.kt +++ b/driver-app/app/src/main/java/de/motis/prima/Nav.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController @@ -15,8 +16,10 @@ import de.motis.prima.services.CookieStore fun Nav() { val navController = rememberNavController() + val vehiclesViewModel: VehiclesViewModel = viewModel() + val toursViewModel: ToursViewModel = viewModel() - // Check before render of any component whether user is authenticated. + // Before rendering any component, check whether user is authenticated. val startDestination by remember { derivedStateOf { val cookieStore = CookieStore(DriversApp.instance) @@ -25,7 +28,11 @@ fun Nav() { "login" } else { Log.d("Cookie", "Cookie found. Navigating to Journeys.") - "home" + if (vehiclesViewModel.selectedVehicleId == 0) { + "vehicles" + } else { + "tours" + } } } } @@ -33,19 +40,35 @@ fun Nav() { NavHost(navController = navController, startDestination = startDestination) { composable(route = "login") { - Login(navController) + Login(navController, vehiclesViewModel) } composable(route = "home") { - Home(navController) + Home(navController, vehiclesViewModel) } composable(route = "vehicles") { - Vehicles(navController) + Vehicles(navController, vehiclesViewModel) } composable(route = "tours") { - Tours(navController) + Tours(navController, vehiclesViewModel, toursViewModel) + } + + composable(route = "taxameter") { + Taxameter(navController, toursViewModel) + } + + composable(route = "overview/{tourId}") { + val tourId = it.arguments?.getString("tourId")?.toInt() + TourOverview(navController, tourId!!) + } + + + composable(route = "legs/{tourId}/{eventIndex}") { + val tourId = it.arguments?.getString("tourId")?.toInt() + val eventIndex = it.arguments?.getString("eventIndex")?.toInt() + TourDetail(navController, tourId!!, eventIndex!!, toursViewModel) } } } diff --git a/driver-app/app/src/main/java/de/motis/prima/Taxameter.kt b/driver-app/app/src/main/java/de/motis/prima/Taxameter.kt new file mode 100644 index 00000000..89ef72e4 --- /dev/null +++ b/driver-app/app/src/main/java/de/motis/prima/Taxameter.kt @@ -0,0 +1,194 @@ +package de.motis.prima + +import android.util.Log +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.layout.wrapContentHeight +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.Button +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.navigation.NavController +import de.motis.prima.services.Api +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch + +class TaxameterViewModel : ViewModel() { + private val _networkErrorEvent = MutableSharedFlow() + val networkErrorEvent = _networkErrorEvent.asSharedFlow() +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun Taxameter( + navController: NavController, + toursViewModel: ToursViewModel, + viewModel: TaxameterViewModel = androidx.lifecycle.viewmodel.compose.viewModel() +) { + var entryCorrect by remember { mutableStateOf(false) } + var fare by remember { mutableStateOf("") } + val snackbarHostState = remember { SnackbarHostState() } + val networkErrorMessage = stringResource(id = R.string.network_error_message) + + LaunchedEffect(key1 = toursViewModel) { + launch { + toursViewModel.logoutEvent.collect { + Log.d("Logout", "Logout event triggered.") + navController.navigate("login") { + launchSingleTop = true + } + } + } + + // Catching event when a network error occurs and displaying of error message + launch { + viewModel.networkErrorEvent.collect { + snackbarHostState.showSnackbar(message = networkErrorMessage) + } + } + } + + var dropdownExpanded by remember { + mutableStateOf(false) + } + + Scaffold( + topBar = { + CenterAlignedTopAppBar( + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary, + ), + title = { + Text( + "Taxameter", + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + navigationIcon = { + /*IconButton(onClick = { navController.navigate("home") }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Localized description" + ) + }*/ + }, + actions = { + IconButton(onClick = { dropdownExpanded = !dropdownExpanded }) { + Icon(Icons.Filled.MoreVert, contentDescription = "More Options") + } + DropdownMenu( + expanded = dropdownExpanded, + onDismissRequest = { dropdownExpanded = false } + ) { + DropdownMenuItem( + onClick = { + navController.navigate("home") + dropdownExpanded = false + + }, + text = { Text("Fahrt abbrechen") } + ) + DropdownMenuItem( + onClick = { + toursViewModel.logout() + dropdownExpanded = false + + }, + text = { Text("Logout") } + ) + } + } + ) + + }, + snackbarHost = { + BoxWithConstraints(modifier = Modifier.fillMaxSize()) { + SnackbarHost( + hostState = snackbarHostState, + modifier = Modifier + .fillMaxSize() + .wrapContentHeight(Alignment.Top) + .padding(top = maxHeight * 0.25f) + ) + } + } + ) { contentPadding -> + ConstraintLayout( + modifier = Modifier + .fillMaxSize() + .padding(contentPadding) + ) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(20.dp)) + OutlinedTextField( + value = fare, + onValueChange = { + fare = it + entryCorrect = false + }, + label = { Text(stringResource(id = R.string.fare_label)) }, + maxLines = 1, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + isError = entryCorrect + ) + Spacer(modifier = Modifier.height(20.dp)) + Button( + onClick = { + entryCorrect = false + Log.d("Taxameter", "Fare: ${fare}") + // TODO: send fare to API + navController.navigate("tours") + } + ) { + Text( + text = stringResource(id = R.string.send_fare_button_text), fontSize = 18.sp + ) + } + } + } + } +} diff --git a/driver-app/app/src/main/java/de/motis/prima/TourDetail.kt b/driver-app/app/src/main/java/de/motis/prima/TourDetail.kt new file mode 100644 index 00000000..fb92e406 --- /dev/null +++ b/driver-app/app/src/main/java/de/motis/prima/TourDetail.kt @@ -0,0 +1,423 @@ +package de.motis.prima + +import android.content.res.Configuration +import android.util.Log +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +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.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.Button +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.navigation.NavController +import de.motis.prima.app.DriversApp +import de.motis.prima.services.CookieStore +import de.motis.prima.services.Event +import de.motis.prima.services.Tour +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch + +class TourDetailViewModel : ViewModel() { + private val cookieStore: CookieStore = CookieStore(DriversApp.instance) + + private val _logoutEvent = MutableSharedFlow() + val logoutEvent = _logoutEvent.asSharedFlow() + + fun logout() { + viewModelScope.launch { + try { + cookieStore.clearCookies() + _logoutEvent.emit(Unit) + } catch (e: Exception) { + Log.d("Logout", "Error while logout.") + } + } + } +} + +@Composable +fun PortraitLayout( + navController: NavController, + tourId: Int, + eventIndex: Int, + toursViewModel: ToursViewModel, + contentPadding: PaddingValues +) { + Log.d("rotation", "portrait") + + var isLastEvent = false + val tour = toursViewModel.tours.value.filter { t -> t.tour_id == tourId }[0] + val event = tour.events[eventIndex] + //val toEvent = tour.events[eventIndex + 1] + + if (eventIndex + 1 == tour.events.size) { + isLastEvent = true + } + + Column( + modifier = Modifier + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + Box( + modifier = Modifier + .padding(contentPadding) + ) { + Text( + text = "${eventIndex + 1} / ${tour.events.size}", + fontSize = 24.sp, + textAlign = TextAlign.Center + ) + } + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + Box( + modifier = Modifier + .padding(contentPadding) + ) { + EventDetail(event) + } + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + if (!isLastEvent) { + Box( + modifier = Modifier + .padding(contentPadding) + ) { + Button( + modifier = Modifier.width(300.dp), + onClick = { + navController.navigate("legs/${tour.tour_id}/${eventIndex + 1}") {} + } + ) { + Text( + text = "Weiter", + fontSize = 24.sp, + textAlign = TextAlign.Center + ) + } + } + } else { + Box( + modifier = Modifier + .padding(contentPadding) + ) { + Button( + modifier = Modifier.width(300.dp), + onClick = { + navController.navigate("taxameter") {} + } + ) { + Text( + text = "Fahrt abgeschlossen", + fontSize = 24.sp, + textAlign = TextAlign.Center + ) + } + } + } + } + } +} + +@Composable +fun LandscapeLayout( + navController: NavController, + tourId: Int, + eventIndex: Int, + toursViewModel: ToursViewModel +) { + // Define UI elements for landscape layout + Log.d("rotation", "landscape") + + Row( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + Column( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .background(Color.LightGray), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text("Row 1", fontSize = 20.sp, modifier = Modifier.padding(16.dp)) + } + + Column( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .background(Color.Gray), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text("Row 2", fontSize = 20.sp, modifier = Modifier.padding(16.dp)) + } + + Column( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .background(Color.DarkGray), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text("Row 3", fontSize = 20.sp, modifier = Modifier.padding(16.dp)) + } + } +} + +@Composable +fun HandleOrientationChanges( + navController: NavController, + tourId: Int, + eventIndex: Int, + toursViewModel: ToursViewModel, + contentPadding: PaddingValues +) { + val configuration = LocalConfiguration.current + val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + if (isLandscape) { + LandscapeLayout( + navController, + tourId, + eventIndex, + toursViewModel + ) + } else { + PortraitLayout( + navController, + tourId, + eventIndex, + toursViewModel, + contentPadding + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TourDetail( + navController: NavController, + tourId: Int, + eventIndex: Int, + toursViewModel: ToursViewModel +) { + LaunchedEffect(key1 = toursViewModel) { + launch { + toursViewModel.logoutEvent.collect { + Log.d("Logout", "Logout event triggered.") + navController.navigate("login") { + launchSingleTop = true + } + } + } + } + + var dropdownExpanded by remember { + mutableStateOf(false) + } + + Scaffold( + topBar = { + CenterAlignedTopAppBar( + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary, + ), + title = { + Text( + "Fahrt", + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + navigationIcon = {}, + actions = { + IconButton(onClick = { dropdownExpanded = !dropdownExpanded }) { + Icon(Icons.Filled.MoreVert, contentDescription = "More Options") + } + DropdownMenu( + expanded = dropdownExpanded, + onDismissRequest = { dropdownExpanded = false } + ) { + DropdownMenuItem( + onClick = { + navController.navigate("home") + dropdownExpanded = false + + }, + text = { Text("Fahrt abbrechen") } + ) + DropdownMenuItem( + onClick = { + toursViewModel.logout() + dropdownExpanded = false + + }, + text = { Text("Logout") } + ) + } + } + ) + + } + ) { contentPadding -> + HandleOrientationChanges( + navController, + tourId, + eventIndex, + toursViewModel, + contentPadding + ) + Box( + modifier = Modifier + .padding(contentPadding) + ) + } +} + +@Composable +fun EventDetail(event: Event) { + var scheduledTime = "" + var city = "" + var street = "" + var houseNumber = "" + var isPickup = false + var firstName = "" + var lastName = "" + var phone = "" + + try { + scheduledTime = event.scheduled_time.split("T")[1].substring(0, 5) + city = event.city + street = event.street + houseNumber = event.house_number + isPickup = event.is_pickup + firstName = event.first_name + lastName = event.last_name + phone = event.phone + } catch (e: Exception) { + Log.d("Exception", "Failed to read event details") + return + } + + Column { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + Text( + text = scheduledTime, + fontSize = 24.sp, + textAlign = TextAlign.Center + ) + } + + if (city == "" || street == "") { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + Text( + text = "Fehler: Keine Addresse", + fontSize = 24.sp, + textAlign = TextAlign.Center + ) + } + } else { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + Text( + text = city, + fontSize = 24.sp, + textAlign = TextAlign.Center + ) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + Text( + text = "$street $houseNumber", + fontSize = 24.sp, + textAlign = TextAlign.Center + ) + } + } + + if (isPickup) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + Text( + text = "$lastName, $firstName", + fontSize = 24.sp, + textAlign = TextAlign.Center + ) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + Text( + text = phone, + fontSize = 24.sp, + textAlign = TextAlign.Center + ) + } + } + } +} diff --git a/driver-app/app/src/main/java/de/motis/prima/TourOverview.kt b/driver-app/app/src/main/java/de/motis/prima/TourOverview.kt new file mode 100644 index 00000000..48a829dc --- /dev/null +++ b/driver-app/app/src/main/java/de/motis/prima/TourOverview.kt @@ -0,0 +1,165 @@ +package de.motis.prima + +import android.util.Log +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.Button +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.navigation.NavController +import de.motis.prima.app.DriversApp +import de.motis.prima.services.CookieStore +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TourOverview( + navController: NavController, + tourId: Int, + viewModel: HomeViewModel = androidx.lifecycle.viewmodel.compose.viewModel() +) { + LaunchedEffect(key1 = viewModel) { + launch { + viewModel.logoutEvent.collect { + Log.d("Logout", "Logout event triggered.") + navController.navigate("login") { + launchSingleTop = true + } + } + } + } + + var dropdownExpanded by remember { + mutableStateOf(false) + } + + Scaffold( + topBar = { + CenterAlignedTopAppBar( + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary, + ), + title = { + Text( + "Fahrt Übersicht", + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + navigationIcon = { + IconButton(onClick = { navController.navigate("home") }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Localized description" + ) + } + }, + actions = { + IconButton(onClick = { dropdownExpanded = !dropdownExpanded }) { + Icon(Icons.Filled.MoreVert, contentDescription = "More Options") + } + DropdownMenu( + expanded = dropdownExpanded, + onDismissRequest = { dropdownExpanded = false } + ) { + DropdownMenuItem( + onClick = { + dropdownExpanded = false + navController.navigate("vehicles") + + }, + text = {Text("Fahrzeug wechseln")} + ) + DropdownMenuItem( + onClick = { + viewModel.logout() + dropdownExpanded = false + + }, + text = { Text("Logout") } + ) + } + } + ) + + } + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + ) { + Spacer(modifier = Modifier.height(42.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + Box() { + Text( + text = "Details zur gesamten Tour", + fontSize = 24.sp, + textAlign = TextAlign.Center + ) + } + } + // TODO + + Spacer(modifier = Modifier.height(36.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + Button( + modifier = Modifier.width(300.dp), + onClick = { + // navController.navigate("tours") {} + navController.navigate("legs/${tourId}/0") + } + ) { + Text( + text = "Fahrt starten", + fontSize = 24.sp, + textAlign = TextAlign.Center + ) + } + } + } + } +} diff --git a/driver-app/app/src/main/java/de/motis/prima/Tours.kt b/driver-app/app/src/main/java/de/motis/prima/Tours.kt index dfc9353f..50c142bf 100644 --- a/driver-app/app/src/main/java/de/motis/prima/Tours.kt +++ b/driver-app/app/src/main/java/de/motis/prima/Tours.kt @@ -1,36 +1,60 @@ package de.motis.prima import android.util.Log -import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row 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.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.ExitToApp -import androidx.compose.material3.Button +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.Card +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.constraintlayout.compose.ConstraintLayout import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.NavController import de.motis.prima.app.DriversApp +import de.motis.prima.services.Api import de.motis.prima.services.CookieStore +import de.motis.prima.services.Tour import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.launch +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import java.text.SimpleDateFormat +import java.util.Date class ToursViewModel : ViewModel() { private val cookieStore: CookieStore = CookieStore(DriversApp.instance) @@ -38,6 +62,37 @@ class ToursViewModel : ViewModel() { private val _logoutEvent = MutableSharedFlow() val logoutEvent = _logoutEvent.asSharedFlow() + var tours = mutableStateOf>(emptyList()) + private set + + var isLoading = mutableStateOf(true) + private set + + init { + fetchTours() + } + + fun fetchTours() { + val currentDate = SimpleDateFormat("yyyy-MM-dd").format(Date()) + Log.d("date", currentDate) + viewModelScope.launch { + Api.apiService.getTours(currentDate).enqueue(object : Callback> { + override fun onResponse(call: Call>, response: Response>) { + if (response.isSuccessful) { + tours.value = response.body() ?: emptyList() + isLoading.value = false + } else { + isLoading.value = false + } + } + + override fun onFailure(call: Call>, t: Throwable) { + isLoading.value = false + } + }) + } + } + fun logout() { viewModelScope.launch { try { @@ -50,12 +105,16 @@ class ToursViewModel : ViewModel() { } } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun Tours( navController: NavController, - viewModel: VehiclesViewModel = androidx.lifecycle.viewmodel.compose.viewModel() + vehiclesViewModel: VehiclesViewModel, + viewModel: ToursViewModel ) { LaunchedEffect(key1 = viewModel) { + viewModel.fetchTours() + Log.d("fetch", "tours") launch { viewModel.logoutEvent.collect { Log.d("Logout", "Logout event triggered.") @@ -66,35 +125,123 @@ fun Tours( } } + var dropdownExpanded by remember { + mutableStateOf(false) + } + Scaffold( - modifier = Modifier - .fillMaxSize() - ) { contentPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(contentPadding) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.End - ) { - TextButton(onClick = { viewModel.logout() }) { - Icon( - Icons.AutoMirrored.Outlined.ExitToApp, contentDescription = null + topBar = { + CenterAlignedTopAppBar( + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary, + ), + title = { + Text( + "Aufträge", + maxLines = 1, + overflow = TextOverflow.Ellipsis ) + }, + navigationIcon = { + IconButton(onClick = { navController.navigate("home") }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Localized description" + ) + } + }, + actions = { + IconButton(onClick = { dropdownExpanded = !dropdownExpanded }) { + Icon(Icons.Filled.MoreVert, contentDescription = "More Options") + } + DropdownMenu( + expanded = dropdownExpanded, + onDismissRequest = { dropdownExpanded = false } + ) { + DropdownMenuItem( + onClick = { + dropdownExpanded = false + navController.navigate("vehicles") + + }, + text = {Text("Fahrzeug wechseln")} + ) + DropdownMenuItem( + onClick = { + viewModel.logout() + dropdownExpanded = false + + }, + text = { Text("Logout") } + ) + } } - } - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center + ) + + } + ) { contentPadding -> + if (vehiclesViewModel.selectedVehicleId == 0) { + Box( + modifier = Modifier.fillMaxSize().padding(16.dp), + contentAlignment = Alignment.Center ) { Text( - text = "Liste der Aufträge", fontSize = 24.sp + text = "Kein Fahrzeug ausgewählt", + fontSize = 24.sp, + textAlign = TextAlign.Center ) } } + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(contentPadding) + ) { + val toursForVehicle = viewModel.tours.value.filter { t -> + t.vehicle_id == vehiclesViewModel.selectedVehicleId + } + Log.d("test", viewModel.tours.value.toString()) + items(items = toursForVehicle, itemContent = { tour -> + ConstraintLayout(modifier = Modifier.clickable { + //navController.navigate("legs/${tour.tour_id}/0") + navController.navigate("overview/${tour.tour_id}") + }) { + var startAddress = "" + var displayTime = "" + try { + val startEvent = tour.events[0] + startAddress = startEvent.city + ", " + startEvent.street + " " + startEvent.house_number + } catch (e: Exception) { + Log.d("error", "Error: Tour has no events") + } + try { + displayTime = tour.from.split("T")[1].substring(0, 5) + } catch (e: Exception) { + Log.d("error", "Error: No display time") + } + + Card( + shape = RoundedCornerShape(12.dp), + modifier = Modifier + .fillMaxWidth() + .padding(start = 24.dp, end = 24.dp, top = 24.dp, bottom = 0.dp) + .height(100.dp) + ) { + Box( + modifier = Modifier.fillMaxSize().padding(16.dp), + contentAlignment = Alignment.TopStart + ) { + Column { + Text(displayTime, fontSize = 24.sp, fontWeight = FontWeight.Bold) + Spacer(modifier = Modifier.height(12.dp)) + Text(startAddress, fontSize = 24.sp) + } + } + } + } + }) + } } } diff --git a/driver-app/app/src/main/java/de/motis/prima/Vehicles.kt b/driver-app/app/src/main/java/de/motis/prima/Vehicles.kt index 67b939bf..8279ea50 100644 --- a/driver-app/app/src/main/java/de/motis/prima/Vehicles.kt +++ b/driver-app/app/src/main/java/de/motis/prima/Vehicles.kt @@ -1,36 +1,55 @@ package de.motis.prima import android.util.Log -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box 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.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.ExitToApp -import androidx.compose.material3.Button +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.Card +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.constraintlayout.compose.ConstraintLayout import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.NavController import de.motis.prima.app.DriversApp +import de.motis.prima.services.Api import de.motis.prima.services.CookieStore +import de.motis.prima.services.Vehicle import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.launch +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response class VehiclesViewModel : ViewModel() { private val cookieStore: CookieStore = CookieStore(DriversApp.instance) @@ -38,6 +57,39 @@ class VehiclesViewModel : ViewModel() { private val _logoutEvent = MutableSharedFlow() val logoutEvent = _logoutEvent.asSharedFlow() + var vehicles = mutableStateOf>(emptyList()) + private set + + var isLoading = mutableStateOf(true) + private set + + var selectedVehicleId = 0 + + init { + fetchVehicles() + } + + fun fetchVehicles() { + Log.d("test", "fetch vehicles") + viewModelScope.launch { + Api.apiService.getVehicles().enqueue(object : Callback> { + override fun onResponse(call: Call>, response: Response>) { + if (response.isSuccessful) { + vehicles.value = response.body() ?: emptyList() + isLoading.value = false + } else { + isLoading.value = false + } + } + + override fun onFailure(call: Call>, t: Throwable) { + isLoading.value = false + } + }) + } + } + + fun logout() { viewModelScope.launch { try { @@ -50,12 +102,14 @@ class VehiclesViewModel : ViewModel() { } } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun Vehicles( navController: NavController, - viewModel: VehiclesViewModel = androidx.lifecycle.viewmodel.compose.viewModel() + viewModel: VehiclesViewModel ) { LaunchedEffect(key1 = viewModel) { + viewModel.fetchVehicles() launch { viewModel.logoutEvent.collect { Log.d("Logout", "Logout event triggered.") @@ -66,35 +120,80 @@ fun Vehicles( } } + var dropdownExpanded by remember { + mutableStateOf(false) + } + Scaffold( - modifier = Modifier - .fillMaxSize() + topBar = { + CenterAlignedTopAppBar( + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary, + ), + title = { + Text( + "Fahrzeuge", + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + navigationIcon = { + IconButton(onClick = { navController.navigate("home") }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Localized description" + ) + } + }, + actions = { + IconButton(onClick = { dropdownExpanded = !dropdownExpanded }) { + Icon(Icons.Filled.MoreVert, contentDescription = "More Options") + } + DropdownMenu( + expanded = dropdownExpanded, + onDismissRequest = { dropdownExpanded = false } + ) { + DropdownMenuItem( + onClick = { + viewModel.logout() + dropdownExpanded = false + + }, + text = { Text("Logout") } + ) + } + } + ) + + } ) { contentPadding -> - Column( + LazyColumn( modifier = Modifier - .fillMaxSize() .padding(contentPadding) ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.End - ) { - TextButton(onClick = { viewModel.logout() }) { - Icon( - Icons.AutoMirrored.Outlined.ExitToApp, contentDescription = null - ) + items(items = viewModel.vehicles.value, itemContent = { vehicle -> + ConstraintLayout(modifier = Modifier.clickable { + viewModel.selectedVehicleId = vehicle.id + navController.navigate("tours") + }) { + Card( + shape = RoundedCornerShape(12.dp), + modifier = Modifier + .fillMaxWidth() + .padding(start = 24.dp, end = 24.dp, top = 24.dp, bottom = 0.dp) + .height(80.dp) + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center // Center content within the Box + ) { + Text(vehicle.license_plate, fontSize = 24.sp, fontWeight = FontWeight.Bold) + } + } } - } - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - Text( - text = "Liste der Fahrzeuge", fontSize = 24.sp - ) - } + + }) } } } diff --git a/driver-app/app/src/main/java/de/motis/prima/services/Api.kt b/driver-app/app/src/main/java/de/motis/prima/services/Api.kt index c688533b..cb894cd4 100644 --- a/driver-app/app/src/main/java/de/motis/prima/services/Api.kt +++ b/driver-app/app/src/main/java/de/motis/prima/services/Api.kt @@ -1,11 +1,15 @@ package de.motis.prima.services import de.motis.prima.BuildConfig.BASE_URL +import retrofit2.Call import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import retrofit2.http.Field import retrofit2.http.FormUrlEncoded +import retrofit2.http.GET import retrofit2.http.POST +import retrofit2.http.Query +import java.util.Date interface ApiService { @POST("login") @@ -14,6 +18,12 @@ interface ApiService { @Field("email") email: String, @Field("password") password: String ): LoginResponse + + @GET("api/vehicle") + fun getVehicles() : Call> + + @GET("api/tour") + fun getTours(@Query("date") currentDate: String) : Call> } object Api { @@ -31,3 +41,42 @@ data class LoginResponse( val status: Int, val data: String ) + +data class Vehicle( + val id: Int, + val license_plate: String, + val company: Int, + val seats: Int, + val wheelchair_capacity: Int, + val bike_capacity: Int, + val storage_space: Int +) + +data class Event( + val address: Int, + val latitude: Double, + val longitude: Double, + val street: String, + val postal_code: String, + val city: String, + val scheduled_time: String, + val house_number: String, + val first_name: String, + val last_name: String, + val phone: String, + val is_pickup: Boolean, + val customer_id: String +) + +data class Tour( + val tour_id: Int, + val from: String, + val to: String, + val vehicle_id: Int, + val license_plate: String, + val company_id: Int, + val fare: Int, + val fare_route: Int, + val company_name: String, + val events: List +) diff --git a/driver-app/app/src/main/res/values-night/themes.xml b/driver-app/app/src/main/res/values-night/themes.xml index 90a777e6..c8d2d108 100644 --- a/driver-app/app/src/main/res/values-night/themes.xml +++ b/driver-app/app/src/main/res/values-night/themes.xml @@ -1,6 +1,6 @@ -