Compare commits

...
2 Commits
Author SHA1 Message Date
admin 1a26817d2c Dodaj ekran startowy podczas sprawdzania sesji 2026-07-01 01:24:59 +02:00
admin 693f879078 Odśwież ekran logowania i ikonę aplikacji 2026-07-01 01:11:51 +02:00
24 changed files with 227 additions and 25 deletions
+2 -2
View File
@@ -12,8 +12,8 @@ android {
applicationId = "pl.firmatpp.kierowca" applicationId = "pl.firmatpp.kierowca"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 12 versionCode = 14
versionName = "1.0.11" versionName = "1.0.13"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField("String", "API_BASE_URL", "\"https://api-intranet.firmatpp.pl/api/\"") buildConfigField("String", "API_BASE_URL", "\"https://api-intranet.firmatpp.pl/api/\"")
+2
View File
@@ -7,7 +7,9 @@
<application <application
android:allowBackup="false" android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.TppKierowca"> android:theme="@style/Theme.TppKierowca">
<activity <activity
@@ -18,6 +18,7 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -46,6 +47,8 @@ import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.AddPhotoAlternate import androidx.compose.material.icons.outlined.AddPhotoAlternate
import androidx.compose.material.icons.outlined.ArrowBack import androidx.compose.material.icons.outlined.ArrowBack
@@ -92,11 +95,14 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.foundation.verticalScroll
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
@@ -129,7 +135,7 @@ fun DriverApp(viewModel: DriverViewModel) {
val lifecycleOwner = LocalLifecycleOwner.current val lifecycleOwner = LocalLifecycleOwner.current
var appInForeground by remember { mutableStateOf(true) } var appInForeground by remember { mutableStateOf(true) }
BackHandler(enabled = state.screen != DriverScreen.Phone && state.screen != DriverScreen.Routes) { BackHandler(enabled = state.screen != DriverScreen.Initializing && state.screen != DriverScreen.Phone && state.screen != DriverScreen.Routes) {
viewModel.back() viewModel.back()
} }
DisposableEffect(lifecycleOwner, state.screen) { DisposableEffect(lifecycleOwner, state.screen) {
@@ -162,6 +168,7 @@ fun DriverApp(viewModel: DriverViewModel) {
Box(Modifier.fillMaxSize().background(TppColors.Surface)) { Box(Modifier.fillMaxSize().background(TppColors.Surface)) {
when (state.screen) { when (state.screen) {
DriverScreen.Initializing -> StartupScreen()
DriverScreen.Phone -> PhoneScreen(state, viewModel::requestOtp) DriverScreen.Phone -> PhoneScreen(state, viewModel::requestOtp)
DriverScreen.Otp -> OtpScreen(state, viewModel::updateOtpCode, viewModel::verifyOtp, viewModel::back) DriverScreen.Otp -> OtpScreen(state, viewModel::updateOtpCode, viewModel::verifyOtp, viewModel::back)
DriverScreen.Routes -> RoutesScreen( DriverScreen.Routes -> RoutesScreen(
@@ -176,7 +183,7 @@ fun DriverApp(viewModel: DriverViewModel) {
DriverScreen.Photo -> PhotoScreen(state, viewModel::back) DriverScreen.Photo -> PhotoScreen(state, viewModel::back)
} }
if (state.loading) { if (state.loading && state.screen != DriverScreen.Initializing) {
Box(Modifier.fillMaxSize().background(Color.White.copy(alpha = 0.42f)), contentAlignment = Alignment.Center) { Box(Modifier.fillMaxSize().background(Color.White.copy(alpha = 0.42f)), contentAlignment = Alignment.Center) {
CircularProgressIndicator(color = TppColors.Forest) CircularProgressIndicator(color = TppColors.Forest)
} }
@@ -185,21 +192,166 @@ fun DriverApp(viewModel: DriverViewModel) {
} }
@Composable @Composable
private fun PhoneScreen(state: DriverUiState, onSubmit: (String) -> Unit) { private fun StartupScreen() {
var phone by remember { mutableStateOf(state.phone) } Column(
AuthShell(title = "TPP Kierowca", subtitle = "Zaloguj sie numerem telefonu kierowcy.") { Modifier
OutlinedTextField( .fillMaxSize()
value = phone, .statusBarsPadding()
onValueChange = { phone = it }, .navigationBarsPadding()
label = { Text("Numer telefonu") }, .padding(32.dp),
leadingIcon = { Icon(Icons.Outlined.Phone, contentDescription = null) }, horizontalAlignment = Alignment.CenterHorizontally,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone), verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth(), ) {
singleLine = true, Image(
painter = painterResource(R.drawable.tpp5),
contentDescription = "Firma TPP",
contentScale = ContentScale.Fit,
modifier = Modifier
.fillMaxWidth()
.height(120.dp),
)
Spacer(Modifier.height(32.dp))
CircularProgressIndicator(color = TppColors.Forest)
Spacer(Modifier.height(20.dp))
Text(
"Sprawdzanie sesji",
color = TppColors.Muted,
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.SemiBold,
)
}
}
@Composable
private fun PhoneScreen(state: DriverUiState, onSubmit: (String) -> Unit) {
var phoneDigits by remember(state.phone) { mutableStateOf(polishPhoneDigits(state.phone)) }
Box(
Modifier
.fillMaxSize()
.background(TppColors.Surface)
.statusBarsPadding()
.navigationBarsPadding()
.padding(24.dp),
) {
Card(
modifier = Modifier.fillMaxSize(),
colors = CardDefaults.cardColors(containerColor = Color(0xFFFEFFFC)),
border = BorderStroke(1.dp, TppColors.Outline),
shape = RoundedCornerShape(4.dp),
) {
Column(
Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(horizontal = 28.dp, vertical = 36.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
painter = painterResource(R.drawable.tpp5),
contentDescription = "Firma TPP",
contentScale = ContentScale.Fit,
modifier = Modifier
.fillMaxWidth()
.height(132.dp),
)
Spacer(Modifier.height(42.dp))
Text(
"Zaloguj się",
style = MaterialTheme.typography.headlineLarge,
color = TppColors.Muted,
fontWeight = FontWeight.Bold,
)
Spacer(Modifier.height(76.dp))
Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(18.dp)) {
Text(
"Numer Telefonu",
color = Color.Black,
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold,
fontSize = 23.sp,
)
PhoneNumberField(
digits = phoneDigits,
onDigitsChange = { phoneDigits = polishPhoneDigits(it) },
)
Text(
"Otrzymasz kod SMS do weryfikacji.",
color = TppColors.Muted,
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.SemiBold,
fontSize = 18.sp,
)
ErrorText(state.error)
Spacer(Modifier.height(if (state.error.isNullOrBlank()) 22.dp else 8.dp))
Button(
onClick = { onSubmit("+48$phoneDigits") },
modifier = Modifier
.fillMaxWidth()
.height(72.dp),
colors = ButtonDefaults.buttonColors(containerColor = TppColors.ContainerGreen),
shape = RoundedCornerShape(4.dp),
contentPadding = PaddingValues(horizontal = 20.dp),
) {
Text(
"Wyślij kod OTP",
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold,
fontSize = 22.sp,
)
}
}
}
}
}
}
@Composable
private fun PhoneNumberField(digits: String, onDigitsChange: (String) -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(82.dp)
.background(Color(0xFFFEFFFC), RoundedCornerShape(2.dp))
.border(BorderStroke(1.dp, TppColors.Outline), RoundedCornerShape(2.dp)),
verticalAlignment = Alignment.CenterVertically,
) {
Box(
Modifier
.width(94.dp)
.fillMaxHeight()
.background(TppColors.Panel),
contentAlignment = Alignment.Center,
) {
Text("+48", color = TppColors.Ink, fontSize = 26.sp)
}
Box(Modifier.width(1.dp).fillMaxHeight().background(TppColors.Outline))
BasicTextField(
value = formatPolishPhoneDigits(digits),
onValueChange = { onDigitsChange(it) },
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone),
textStyle = TextStyle(
color = TppColors.Muted,
fontSize = 28.sp,
fontWeight = FontWeight.Medium,
),
modifier = Modifier
.weight(1f)
.padding(horizontal = 24.dp),
decorationBox = { innerTextField ->
Box(contentAlignment = Alignment.CenterStart) {
if (digits.isBlank()) {
Text(
"000 000 000",
color = TppColors.Muted.copy(alpha = 0.92f),
fontSize = 28.sp,
fontWeight = FontWeight.Medium,
)
}
innerTextField()
}
},
) )
ErrorText(state.error)
Spacer(Modifier.height(if (state.error.isNullOrBlank()) 18.dp else 8.dp))
PrimaryButton("Wyslij kod") { onSubmit(phone) }
} }
} }
@@ -998,6 +1150,16 @@ private fun ErrorText(error: String?) {
if (!error.isNullOrBlank()) Text(error, color = TppColors.Error, modifier = Modifier.padding(vertical = 8.dp)) if (!error.isNullOrBlank()) Text(error, color = TppColors.Error, modifier = Modifier.padding(vertical = 8.dp))
} }
private fun polishPhoneDigits(value: String): String {
val digits = value.filter(Char::isDigit)
val withoutCountryCode = if (digits.length > 9 && digits.startsWith("48")) digits.drop(2) else digits
return withoutCountryCode.take(9)
}
private fun formatPolishPhoneDigits(value: String): String =
value.chunked(3).joinToString(" ")
private fun createCameraUri(context: Context): Uri { private fun createCameraUri(context: Context): Uri {
val dir = File(context.cacheDir, "camera").apply { mkdirs() } val dir = File(context.cacheDir, "camera").apply { mkdirs() }
val file = File.createTempFile("ladunek-", ".jpg", dir) val file = File.createTempFile("ladunek-", ".jpg", dir)
@@ -15,11 +15,11 @@ import pl.firmatpp.kierowca.data.model.DriverDto
import pl.firmatpp.kierowca.data.model.DriverRouteDto import pl.firmatpp.kierowca.data.model.DriverRouteDto
import pl.firmatpp.kierowca.data.model.RoutePhotoDto import pl.firmatpp.kierowca.data.model.RoutePhotoDto
enum class DriverScreen { Phone, Otp, Routes, Profile, Detail, Photo } enum class DriverScreen { Initializing, Phone, Otp, Routes, Profile, Detail, Photo }
data class DriverUiState( data class DriverUiState(
val screen: DriverScreen = DriverScreen.Phone, val screen: DriverScreen = DriverScreen.Initializing,
val loading: Boolean = false, val loading: Boolean = true,
val refreshing: Boolean = false, val refreshing: Boolean = false,
val phone: String = "", val phone: String = "",
val otpCode: String = "", val otpCode: String = "",
@@ -45,7 +45,11 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
init { init {
viewModelScope.launch { viewModelScope.launch {
if (repository.hasToken()) refreshRoutes() else _state.update { it.copy(loading = false) } if (repository.hasToken()) {
refreshRoutes()
} else {
_state.update { it.copy(screen = DriverScreen.Phone, loading = false) }
}
} }
} }
@@ -106,7 +110,12 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
) )
} }
}.onFailure { throwable -> }.onFailure { throwable ->
_state.update { it.copy(error = throwable.message ?: "Wystapil blad.") } _state.update {
it.copy(
screen = if (it.screen == DriverScreen.Initializing) DriverScreen.Phone else it.screen,
error = throwable.message ?: "Wystapil blad.",
)
}
} }
_state.update { _state.update {
@@ -178,7 +187,7 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
fun logout() = runLoading { fun logout() = runLoading {
repository.logout() repository.logout()
_state.update { DriverUiState(screen = DriverScreen.Phone) } _state.update { DriverUiState(screen = DriverScreen.Phone, loading = false) }
} }
private fun runLoading(block: suspend () -> Unit) { private fun runLoading(block: suspend () -> Unit) {
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="#F7FAF8" />
</shape>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
</adaptive-icon>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

+1 -1
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">TPP Kierowca</string> <string name="app_name">Firma TPP - Kierowca</string>
</resources> </resources>
@@ -0,0 +1,15 @@
package pl.firmatpp.kierowca.ui
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
class DriverUiStateTest {
@Test
fun startsOnInitializingScreenBeforeTokenCheckCompletes() {
val state = DriverUiState()
assertEquals(DriverScreen.Initializing, state.screen)
assertTrue(state.loading)
}
}