Compare commits

...

10 Commits

47 changed files with 239686 additions and 71344 deletions

View File

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<runningDeviceTargetSelectedWithDropDown>
<Target>
<type value="RUNNING_DEVICE_TARGET" />
<deviceKey>
<Key>
<type value="SERIAL_NUMBER" />
<value value="KFRSEQ6DTWWWQOE6" />
</Key>
</deviceKey>
</Target>
</runningDeviceTargetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2023-12-17T14:14:18.887820Z" />
</component>
</project>

View File

@ -1,5 +1,6 @@
package com.example.myapplication
import android.content.Context
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
@ -20,6 +21,7 @@ class MainComposeActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
application.deleteDatabase("pmy-db")
appContext = applicationContext
setContent {
PmudemoTheme(darkTheme = isDarkTheme.value) {
LaunchedEffect(key1 = true) {
@ -40,4 +42,7 @@ class MainComposeActivity : ComponentActivity() {
}
}
}
companion object {
lateinit var appContext: Context
}
}

View File

@ -90,6 +90,11 @@ interface MyServerService {
@Query("userId") userId: Int,
): List<UserSessionWithSessionRemote>
@GET("userssessions")
suspend fun getUserSessions(
@Query("userId") userId: Int,
): List<UserSessionRemote>
@GET("userssessions?_expand=session")
suspend fun getUsersSessions(): List<UserSessionWithSessionRemote>

View File

@ -55,7 +55,7 @@ class CinemaRemoteMediator(
try {
val cinemas = service.getCinemas(page, state.config.pageSize).map { it.toCinema() }
val sessionsFromCinemas = cinemas.map { cinema ->
val sessionsFromCinemas = cinemas.flatMap { cinema ->
service.getCinemaWithSessions(cinema.uid).toSessions()
}
val endOfPaginationReached = cinemas.isEmpty()
@ -77,13 +77,7 @@ class CinemaRemoteMediator(
}
dbRemoteKeyRepository.createRemoteKeys(keys)
dbCinemaRepository.insertCinemas(cinemas)
sessionsFromCinemas.forEach {
try {
dbSessionRepository.insertSessions(it)
} catch (_: Exception) {
}
}
dbSessionRepository.insertSessions(sessionsFromCinemas)
}
return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
} catch (exception: IOException) {

View File

@ -48,18 +48,18 @@ class RestCinemaRepository(
override suspend fun getCinema(uid: Int): CinemaWithSessions {
val cinemaWithSessions = service.getCinemaWithSessions(uid)
val orders = service.getOrders()
val sessions = cinemaWithSessions.sessions.map { sessionFromCinemaRemote ->
SessionFromCinema(
sessionFromCinemaRemote.id,
sessionFromCinemaRemote.dateTime,
sessionFromCinemaRemote.price,
sessionFromCinemaRemote.maxCount - service.getOrders().flatMap
sessionFromCinemaRemote.maxCount - orders.flatMap
{ order ->
order.sessions.filter { session ->
session.id == sessionFromCinemaRemote.id &&
session.cinemaId == sessionFromCinemaRemote.cinemaId &&
session.cinema.name == cinemaWithSessions.name
session.dateTime == sessionFromCinemaRemote.dateTime
}
}.sumOf { session -> session.count },
uid

View File

@ -53,8 +53,8 @@ class OrderRemoteMediator(
try {
val orders = service.getOrders(
LiveStore.user.value?.uid ?: 0,
page, state.config.pageSize
userId = LiveStore.user.value?.uid ?: 0,
page = page, limit = state.config.pageSize
).map { it.toOrder() }
val endOfPaginationReached = orders.isEmpty()
database.withTransaction {

View File

@ -24,9 +24,8 @@ class RestOrderRepository(
private val dbRemoteKeyRepository: OfflineRemoteKeyRepository,
private val database: AppDatabase
) : OrderRepository {
override fun getAllOrders(userId: Int): Flow<PagingData<Order>> {
val pagingSourceFactory = { dbOrderRepository.getAllOrdersPagingSource(userId) }
override fun getAllOrders(): Flow<PagingData<Order>> {
val pagingSourceFactory = { dbOrderRepository.getAllOrdersPagingSource() }
@OptIn(ExperimentalPagingApi::class)
return Pager(
config = PagingConfig(

View File

@ -11,7 +11,7 @@ class RestOrderSessionRepository(
private val dbOrderSessionRepository: OfflineOrderSessionRepository
) : OrderSessionRepository {
override suspend fun insertOrderSession(orderSessionCrossRef: OrderSessionCrossRef) {
var orderRemote = service.getOrder(orderSessionCrossRef.orderId)
val orderRemote = service.getOrder(orderSessionCrossRef.orderId)
val session = service.getSession(orderSessionCrossRef.sessionId)
val sessionFromOrder = SessionFromOrderRemote(
@ -23,10 +23,9 @@ class RestOrderSessionRepository(
session.cinema
)
val updatedSessions = orderRemote.sessions.toMutableList()
updatedSessions.add(sessionFromOrder)
orderRemote = orderRemote.copy(sessions = updatedSessions)
orderRemote.sessions = orderRemote.sessions.toMutableList().apply {
add(sessionFromOrder)
}
service.updateOrder(orderSessionCrossRef.orderId, orderRemote)
dbOrderSessionRepository.insertOrderSession(orderSessionCrossRef)
}

View File

@ -37,14 +37,27 @@ class RestUserRepository(
)
)
}
return cart.map {
val cinema = service.getCinema(it.session.cinemaId)
it.toSessionFromCart(
it.session.maxCount - service.getOrders().flatMap { order ->
order.sessions.filter { session -> session.id == it.id }
}.sumOf { session -> session.count }, cinema.toCinema()
val orders = service.getOrders()
val sessions = cart.map { sessionFromCartRemote ->
SessionFromCart(
uid = sessionFromCartRemote.sessionId,
dateTime = sessionFromCartRemote.session.dateTime,
price = sessionFromCartRemote.session.price,
availableCount = sessionFromCartRemote.session.maxCount - orders
.flatMap
{ order ->
order.sessions.filter { session ->
session.id == sessionFromCartRemote.sessionId &&
session.cinemaId == sessionFromCartRemote.session.cinemaId &&
session.dateTime == sessionFromCartRemote.session.dateTime
}
}.sumOf { session -> session.count },
count = sessionFromCartRemote.count,
cinemaId = sessionFromCartRemote.session.cinemaId,
cinema = service.getCinema(sessionFromCartRemote.session.cinemaId).toCinema()
)
}
return sessions
}
override suspend fun insertUser(user: User) {

View File

@ -1,8 +1,6 @@
package com.example.myapplication.api.user
import com.example.myapplication.api.session.SessionRemote
import com.example.myapplication.database.entities.model.Cinema
import com.example.myapplication.database.entities.model.SessionFromCart
import kotlinx.serialization.Serializable
@Serializable
@ -12,17 +10,4 @@ data class UserSessionWithSessionRemote(
val sessionId: Int = 0,
val count: Int = 0,
val session: SessionRemote,
)
fun UserSessionWithSessionRemote.toSessionFromCart(
availableCount: Int = 0,
cinema: Cinema
): SessionFromCart = SessionFromCart(
sessionId,
session.dateTime,
session.price,
availableCount,
count,
session.cinemaId,
cinema
)

View File

@ -46,16 +46,20 @@ class RestUserSessionRepository(
val userSessionRemote = service.getUserSession(
userSessionCrossRef.userId,
userSessionCrossRef.sessionId
).first()
).firstOrNull() ?: return
service.deleteUserSession(userSessionRemote.id)
dbUserSessionRepository.deleteUserSession(userSessionCrossRef)
}
override suspend fun deleteUserSessions(userId: Int) {
val cart = service.getUserCart(userId)
val cart = service.getUserSessions(userId)
cart.forEach {
service.deleteUserSession(it.id)
}
dbUserSessionRepository.deleteUserSessions(userId)
}
override suspend fun deleteUserSessions(userSessionCrossRefs: List<UserSessionCrossRef>) {
userSessionCrossRefs.forEach { deleteUserSession(it) }
}
}

View File

@ -15,21 +15,17 @@ fun Authenticator(
viewModel: AuthenticatorViewModel = viewModel(factory = AppViewModelProvider.Factory)
) {
val scope = rememberCoroutineScope()
val login = dataStoreManager.getLogin().collectAsState(initial = "").value
LiveStore.user.value = viewModel.authUiState.user
fun synchronize() {
scope.launch {
if (login == "") {
LiveStore.user.value = null
return@launch
}
val overlap = viewModel.findUserByLogin(login)
if (overlap == null) {
dataStoreManager.setLogin("")
return@launch
}
LiveStore.user.value = overlap
viewModel.findUserByLogin(login)
}
}

View File

@ -1,18 +1,27 @@
package com.example.myapplication.composeui
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.example.myapplication.database.entities.model.User
import com.example.myapplication.database.entities.repository.UserRepository
class AuthenticatorViewModel(
private val userRepository: UserRepository
) : MyViewModel() {
suspend fun findUserByLogin(login: String): User? {
var user: User? = null
var authUiState by mutableStateOf(AuthenticatorUiState())
private set
suspend fun findUserByLogin(login: String) {
runInScope(
actionSuccess = {
user = userRepository.getUser(login)
authUiState = AuthenticatorUiState(userRepository.getUser(login))
},
actionError = {
authUiState = AuthenticatorUiState()
}
)
return user
}
}
}
data class AuthenticatorUiState(val user: User? = null)

View File

@ -1,6 +1,5 @@
package com.example.myapplication.composeui
import android.content.res.Configuration
import android.graphics.BitmapFactory
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@ -10,6 +9,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
@ -17,16 +17,12 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.Button
import androidx.compose.material3.DismissDirection
import androidx.compose.material3.DismissState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.rememberDismissState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.livedata.observeAsState
@ -37,21 +33,23 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.example.myapplication.LiveStore
import com.example.myapplication.R
import com.example.myapplication.api.ApiStatus
import com.example.myapplication.composeui.navigation.Screen
import com.example.myapplication.database.entities.composeui.AppViewModelProvider
import com.example.myapplication.database.entities.model.Session
import com.example.myapplication.database.entities.model.SessionFromCart
import com.example.myapplication.database.entities.model.UserRole
import com.example.myapplication.ui.theme.PmudemoTheme
import kotlinx.coroutines.launch
import org.threeten.bp.format.DateTimeFormatter
@Composable
fun Cart(
navController: NavController,
viewModel: CartViewModel = viewModel(factory = AppViewModelProvider.Factory)
) {
val coroutineScope = rememberCoroutineScope()
@ -60,65 +58,66 @@ fun Cart(
LaunchedEffect(Unit) {
viewModel.refreshState()
}
Cart(
cartUiState = cartUiState,
modifier = Modifier
.padding(all = 10.dp),
onSwipe = { session: SessionFromCart ->
coroutineScope.launch {
viewModel.removeFromCart(
session = Session(
uid = session.uid,
dateTime = session.dateTime,
price = session.price,
maxCount = 0,
cinemaId = session.cinemaId
)
)
}
},
onChangeCount = { session: SessionFromCart, count: Int ->
coroutineScope.launch {
viewModel.updateFromCart(
session = Session(
uid = session.uid,
dateTime = session.dateTime,
price = session.price,
maxCount = 0,
cinemaId = session.cinemaId
), count = count, availableCount = session.availableCount
)
}
},
onAddToOrder = { sessions: List<SessionFromCart> ->
coroutineScope.launch {
viewModel.addToOrder(sessions = sessions)
}
when (viewModel.apiStatus) {
ApiStatus.DONE -> {
Cart(
cartUiState = cartUiState,
modifier = Modifier
.padding(all = 10.dp),
onChangeCount = { session: SessionFromCart, count: Int ->
coroutineScope.launch {
viewModel.updateFromCart(
session = Session(
uid = session.uid,
dateTime = session.dateTime,
price = session.price,
maxCount = 0,
cinemaId = session.cinemaId
), count = count, availableCount = session.availableCount
)
}
},
onAddToOrder = { sessions: List<SessionFromCart> ->
coroutineScope.launch {
viewModel.addToOrder(sessions = sessions)
}
},
onDelete = { session: SessionFromCart ->
coroutineScope.launch {
viewModel.removeFromCart(
session = Session(
uid = session.uid,
dateTime = session.dateTime,
price = session.price,
maxCount = 0,
cinemaId = session.cinemaId
)
)
}
}
)
}
)
ApiStatus.LOADING -> LoadingPlaceholder()
else -> ErrorPlaceholder(
message = viewModel.apiError,
onBack = { navController.navigate(Screen.Report.route) }
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun Cart(
cartUiState: CartUiState,
modifier: Modifier,
onSwipe: (SessionFromCart) -> Unit,
onChangeCount: (SessionFromCart, Int) -> Unit,
onAddToOrder: (List<SessionFromCart>) -> Unit
onAddToOrder: (List<SessionFromCart>) -> Unit,
onDelete: (SessionFromCart) -> Unit
) {
LazyColumn(
modifier = modifier
) {
items(cartUiState.sessionList, key = { it.uid.toString() }) { session ->
val dismissState: DismissState = rememberDismissState(
positionalThreshold = { 200.dp.toPx() }
)
if (dismissState.isDismissed(direction = DismissDirection.EndToStart)) {
onSwipe(session)
}
SessionListItem(
session = session,
modifier = Modifier
@ -126,9 +125,13 @@ private fun Cart(
.padding(10.dp)
.clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colorScheme.secondary),
onChangeCount = onChangeCount
onChangeCount = onChangeCount,
onDelete = onDelete,
)
}
item {
Spacer(modifier = Modifier.height(48.dp))
}
}
val user = LiveStore.user.observeAsState()
if (user.value?.role == UserRole.USER) {
@ -138,7 +141,7 @@ private fun Cart(
Button(
onClick = { onAddToOrder(cartUiState.sessionList) },
modifier = Modifier
.padding(16.dp)
.padding(6.dp)
.fillMaxWidth()
) { Text("Купить") }
}
@ -150,27 +153,26 @@ private fun SessionListItem(
session: SessionFromCart,
modifier: Modifier = Modifier,
onChangeCount: (SessionFromCart, Int) -> Unit,
onDelete: (SessionFromCart) -> Unit
) {
//var currentCount by remember { mutableIntStateOf(session.count) }
val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")
val formattedDate = dateFormatter.format(session.dateTime)
Column {
Text(
text = formattedDate,
color = MaterialTheme.colorScheme.onBackground,
)
Text(
text = formattedDate,
color = MaterialTheme.colorScheme.onBackground,
)
Column(modifier = modifier.fillMaxWidth()) {
Box(
modifier = modifier
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
if (session.cinema.image != null)
if (session.cinema.image != null) {
Image(
bitmap = BitmapFactory.decodeByteArray(
session.cinema.image,
@ -182,6 +184,7 @@ private fun SessionListItem(
.size(90.dp)
.padding(4.dp)
)
}
Column(
modifier = Modifier.weight(1f),
@ -190,68 +193,66 @@ private fun SessionListItem(
Text(
text = "${session.cinema.name}, ${session.cinema.year}\n" +
"Цена: ${session.price}\n" +
"${session.count}/${session.availableCount}",
if (session.availableCount == 0) "Недоступно" else "${session.count}/${session.availableCount}",
color = MaterialTheme.colorScheme.onSecondary
)
}
}
}
Box(
modifier = Modifier
.background(
color = MaterialTheme.colorScheme.background,
shape = RoundedCornerShape(10.dp)
) // Задаем фон для кнопок
Row(
modifier = Modifier
.background(color = MaterialTheme.colorScheme.primary)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(
onClick = { onDelete(session) }
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Удалить",
tint = MaterialTheme.colorScheme.onPrimary
)
}
Spacer(modifier = Modifier.weight(1F))
if (session.availableCount != 0) {
IconButton(
enabled = session.count != 1,
onClick = { onChangeCount(session, session.count - 1) }
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
IconButton(
onClick = { onChangeCount(session, session.count - 1) }
) {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.minus),
contentDescription = "Уменьшить",
tint = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.size(10.dp)
)
}
Text(
text = "${session.count}",
color = MaterialTheme.colorScheme.onBackground
)
IconButton(
onClick = {
onChangeCount(
session,
if (session.count != session.availableCount) session.count + 1 else session.count
)
}
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "Увеличить",
tint = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.size(10.dp)
)
}
}
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.minus),
contentDescription = "Уменьшить",
tint = MaterialTheme.colorScheme.onPrimary
)
}
Text(
text = "${session.count}",
color = MaterialTheme.colorScheme.onPrimary
)
IconButton(
enabled = session.count != session.availableCount,
onClick = {
onChangeCount(
session,
if (session.count != session.availableCount) session.count + 1 else session.count
)
}
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "Увеличить",
tint = MaterialTheme.colorScheme.onPrimary
)
}
}
}
}
}
@Preview(name = "Light Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun CartPreview() {
PmudemoTheme {
Surface(
color = MaterialTheme.colorScheme.background
) {
Cart()
}
}
}

View File

@ -3,7 +3,7 @@ package com.example.myapplication.composeui
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.room.Transaction
import com.example.myapplication.LiveStore
import com.example.myapplication.database.entities.model.Order
import com.example.myapplication.database.entities.model.OrderSessionCrossRef
@ -21,56 +21,93 @@ class CartViewModel(
private val orderRepository: OrderRepository,
private val orderSessionRepository: OrderSessionRepository,
private val userRepository: UserRepository,
) : ViewModel() {
) : MyViewModel() {
private var isLoading: Boolean = false
var cartUiState by mutableStateOf(CartUiState())
private set
suspend fun refreshState() {
val userId: Int = LiveStore.user.value?.uid ?: 0
val cart = userRepository.getCartByUser(userId)
cartUiState = CartUiState(cart)
suspend fun refreshState(needLoadingScreen: Boolean = true) {
val userId: Int = LiveStore.user.value?.uid ?: return
runInScope(
actionSuccess = {
cartUiState = CartUiState(userRepository.getCartByUser(userId))
}, actionError = {
cartUiState = CartUiState()
},
needLoadingScreen = needLoadingScreen
)
}
@Transaction
suspend fun addToOrder(sessions: List<SessionFromCart>) {
if (isLoading)
return
isLoading = true
val userId: Int = LiveStore.user.value?.uid ?: return
if (sessions.isEmpty())
val cart = sessions.filter { it.availableCount != 0 }
if (cart.isEmpty())
return
val orderId = orderRepository.insertOrder(Order(0, userId, LocalDateTime.now()))
sessions.forEach { session ->
orderSessionRepository.insertOrderSession(
OrderSessionCrossRef(
orderId.toInt(),
session.uid,
session.price,
session.count
)
)
}
userSessionRepository.deleteUserSessions(userId)
refreshState()
runInScope(
actionSuccess = {
val orderId = orderRepository.insertOrder(Order(0, userId, LocalDateTime.now()))
cart.forEach { session ->
if (session.availableCount != 0) {
orderSessionRepository.insertOrderSession(
OrderSessionCrossRef(
orderId.toInt(),
session.uid,
session.price,
session.count
)
)
}
}
userSessionRepository.deleteUserSessions(cart.map {
UserSessionCrossRef(userId, it.uid, it.count)
})
refreshState()
}
)
}
suspend fun removeFromCart(session: Session, count: Int = 0) {
val userId: Int = LiveStore.user.value?.uid ?: return
userSessionRepository.deleteUserSession(UserSessionCrossRef(userId, session.uid, count))
refreshState()
runInScope(
actionSuccess = {
userSessionRepository.deleteUserSession(
UserSessionCrossRef(
userId,
session.uid,
count
)
)
refreshState()
}
)
}
suspend fun updateFromCart(session: Session, count: Int, availableCount: Int): Boolean {
val userId: Int = LiveStore.user.value?.uid ?: return false
suspend fun updateFromCart(session: Session, count: Int, availableCount: Int) {
val userId: Int = LiveStore.user.value?.uid ?: return
if (count == 0) {
removeFromCart(session, count)
return false
return
}
if (count > availableCount)
return false
userSessionRepository.updateUserSession(UserSessionCrossRef(userId, session.uid, count))
refreshState()
return true
return
runInScope(
actionSuccess = {
userSessionRepository.updateUserSession(
UserSessionCrossRef(
userId,
session.uid,
count
)
)
refreshState()
},
actionError = { },
needLoadingScreen = false
)
}
}

View File

@ -19,10 +19,12 @@ open class MyViewModel : ViewModel() {
fun runInScope(
actionSuccess: suspend () -> Unit,
actionError: suspend () -> Unit
actionError: suspend () -> Unit,
needLoadingScreen: Boolean = true,
) {
viewModelScope.launch {
apiStatus = ApiStatus.LOADING
if (needLoadingScreen)
apiStatus = ApiStatus.LOADING
runCatching {
actionSuccess()
apiStatus = ApiStatus.DONE

View File

@ -1,6 +1,5 @@
package com.example.myapplication.composeui
import android.content.res.Configuration
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@ -8,8 +7,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@ -17,12 +14,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.dp
import com.example.myapplication.R
import com.example.myapplication.ui.theme.PmudemoTheme
@Composable
@ -67,30 +62,4 @@ fun LoadingPlaceholder() {
text = stringResource(id = R.string.loading)
)
}
}
@Preview(name = "Light Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun ErrorPlaceholderPreview() {
PmudemoTheme {
Surface(
color = MaterialTheme.colorScheme.background
) {
ErrorPlaceholder("Error", onBack = {})
}
}
}
@Preview(name = "Light Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun LoadingPlaceholderPreview() {
PmudemoTheme {
Surface(
color = MaterialTheme.colorScheme.background
) {
LoadingPlaceholder()
}
}
}

View File

@ -25,7 +25,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.example.myapplication.api.ApiStatus
import com.example.myapplication.api.session.ReportRemote
import com.example.myapplication.composeui.navigation.Screen
import com.example.myapplication.database.entities.composeui.AppViewModelProvider
import kotlinx.coroutines.launch
import org.threeten.bp.format.DateTimeFormatter
@ -34,62 +37,73 @@ import java.util.Date
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Report(
navController: NavController,
viewModel: ReportViewModel = viewModel(factory = AppViewModelProvider.Factory)
) {
val dateStateStart = rememberDatePickerState(initialDisplayMode = DisplayMode.Input)
val dateStateEnd = rememberDatePickerState(initialDisplayMode = DisplayMode.Input)
val coroutineScope = rememberCoroutineScope()
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.fillMaxWidth()
.padding(all = 10.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
)
{
Text(
text = "Начало периода",
style = MaterialTheme.typography.headlineLarge
)
DatePicker(state = dateStateStart)
val selectedDateStart = dateStateStart.selectedDateMillis
if (selectedDateStart != null) {
viewModel.onUpdate(
viewModel.reportUiState.reportDetails.copy(
startDate =
Date(selectedDateStart)
)
when (viewModel.apiStatus) {
ApiStatus.DONE -> {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.fillMaxWidth()
.padding(all = 10.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
)
} else {
viewModel.onUpdate(viewModel.reportUiState.reportDetails.copy(startDate = Date(0)))
}
Text(
text = "Конец периода",
style = MaterialTheme.typography.headlineLarge
)
DatePicker(state = dateStateEnd)
val selectedDateEnd = dateStateEnd.selectedDateMillis
if (selectedDateEnd != null) {
viewModel.onUpdate(
viewModel.reportUiState.reportDetails.copy(
endDate =
Date(selectedDateEnd)
{
Text(
text = "Начало периода",
style = MaterialTheme.typography.headlineLarge
)
)
} else {
viewModel.onUpdate(viewModel.reportUiState.reportDetails.copy(endDate = Date(0)))
DatePicker(state = dateStateStart)
val selectedDateStart = dateStateStart.selectedDateMillis
if (selectedDateStart != null) {
viewModel.onUpdate(
viewModel.reportUiState.reportDetails.copy(
startDate =
Date(selectedDateStart)
)
)
} else {
viewModel.onUpdate(viewModel.reportUiState.reportDetails.copy(startDate = Date(0)))
}
Text(
text = "Конец периода",
style = MaterialTheme.typography.headlineLarge
)
DatePicker(state = dateStateEnd)
val selectedDateEnd = dateStateEnd.selectedDateMillis
if (selectedDateEnd != null) {
viewModel.onUpdate(
viewModel.reportUiState.reportDetails.copy(
endDate =
Date(selectedDateEnd)
)
)
} else {
viewModel.onUpdate(viewModel.reportUiState.reportDetails.copy(endDate = Date(0)))
}
Button(
onClick = { coroutineScope.launch { viewModel.getReport() } },
enabled = viewModel.reportUiState.isEntryValid,
modifier = Modifier.fillMaxWidth()
) {
Text(text = "Получить отчет")
}
Spacer(modifier = Modifier.height(16.dp))
CardScreen(reportData = viewModel.reportResultUiState.report)
}
}
Button(
onClick = { coroutineScope.launch { viewModel.getReport() } },
enabled = viewModel.reportUiState.isEntryValid,
shape = MaterialTheme.shapes.small,
modifier = Modifier.fillMaxWidth()
) {
Text(text = "Получить отчет")
}
Spacer(modifier = Modifier.height(16.dp))
CardScreen(reportData = viewModel.reportResultUiState.report)
ApiStatus.LOADING -> LoadingPlaceholder()
else -> ErrorPlaceholder(
message = viewModel.apiError,
onBack = { navController.navigate(Screen.Report.route) }
)
}
}

View File

@ -3,12 +3,11 @@ package com.example.myapplication.composeui
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import com.example.myapplication.api.session.ReportRemote
import com.example.myapplication.api.session.RestSessionRepository
import java.util.Date
class ReportViewModel(private val serialRepository: RestSessionRepository) : ViewModel() {
class ReportViewModel(private val serialRepository: RestSessionRepository) : MyViewModel() {
var reportUiState by mutableStateOf(ReportUiState())
private set
@ -32,11 +31,17 @@ class ReportViewModel(private val serialRepository: RestSessionRepository) : Vie
suspend fun getReport() {
if (validateInput()) {
val temp = serialRepository.getReport(
reportUiState.reportDetails.startDate,
reportUiState.reportDetails.endDate
runInScope(
actionSuccess = {
val temp = serialRepository.getReport(
reportUiState.reportDetails.startDate,
reportUiState.reportDetails.endDate
)
reportResultUiState = ReportResultUiState(temp)
}, actionError = {
reportResultUiState = ReportResultUiState()
}
)
reportResultUiState = ReportResultUiState(temp)
}
}
}

View File

@ -93,22 +93,21 @@ fun Topbar(
Spacer(modifier = Modifier.width(16.dp))
if (currentScreen?.route == Screen.CinemaList.route) {
Search(
initValue = LiveStore.searchRequest.value ?: "",
onDone = {
LiveStore.searchRequest.value = it
},
modifier = Modifier
.weight(1f)
.height(36.dp)
.background(
color = MaterialTheme.colorScheme.onPrimary,
RoundedCornerShape(18.dp)
)
.padding(start = 13.dp, top = 8.dp)
)
}
Search(
initValue = LiveStore.searchRequest.value ?: "",
onDone = {
navController.navigate(Screen.CinemaList.route)
LiveStore.searchRequest.value = it
},
modifier = Modifier
.weight(1f)
.height(36.dp)
.background(
color = MaterialTheme.colorScheme.onPrimary,
RoundedCornerShape(18.dp)
)
.padding(start = 13.dp, top = 8.dp)
)
}
}
}
@ -163,8 +162,8 @@ fun Navhost(
) {
composable(Screen.CinemaList.route) { CinemaList(navController) }
composable(Screen.OrderList.route) { OrderList(navController) }
composable(Screen.Cart.route) { Cart() }
composable(Screen.UserProfile.route) { UserProfile(isDarkTheme, dataStore, navController) }
composable(Screen.Cart.route) { Cart(navController) }
composable(Screen.UserProfile.route) { UserProfile(isDarkTheme, dataStore) }
composable(
Screen.CinemaEdit.route,
arguments = listOf(navArgument("id") { type = NavType.IntType })
@ -188,9 +187,9 @@ fun Navhost(
Screen.OrderView.route,
arguments = listOf(navArgument("id") { type = NavType.IntType })
) { backStackEntry ->
backStackEntry.arguments?.let { OrderView(it.getInt("id")) }
backStackEntry.arguments?.let { OrderView(navController) }
}
composable(Screen.Report.route) { Report() }
composable(Screen.Report.route) { Report(navController) }
}
}

View File

@ -56,7 +56,6 @@ fun CinemaList(
LaunchedEffect(searchPattern.value) {
viewModel.refresh()
}
Scaffold(
topBar = {},
floatingActionButton = {
@ -99,6 +98,7 @@ fun CinemaList(
}
}
@Composable
private fun CinemaList(
modifier: Modifier = Modifier,

View File

@ -21,12 +21,16 @@ class CinemaListViewModel(
fun refresh() {
val name = "%${LiveStore.searchRequest.value}%"
val pagingSource = cinemaRepository.getAllCinemas(name)
cinemaPagingFlow = CinemaPagingFlowState(pagingSource.cachedIn(viewModelScope))
runInScope(actionSuccess = {
val pagingSource = cinemaRepository.getAllCinemas(name)
cinemaPagingFlow = CinemaPagingFlowState(pagingSource.cachedIn(viewModelScope))
})
}
suspend fun deleteCinema(cinema: Cinema) {
cinemaRepository.deleteCinema(cinema)
runInScope(actionSuccess = {
cinemaRepository.deleteCinema(cinema)
})
}
}

View File

@ -31,6 +31,9 @@ import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.example.myapplication.LiveStore
import com.example.myapplication.api.ApiStatus
import com.example.myapplication.composeui.ErrorPlaceholder
import com.example.myapplication.composeui.LoadingPlaceholder
import com.example.myapplication.composeui.navigation.Screen
import com.example.myapplication.database.entities.model.Cinema
import com.example.myapplication.database.entities.model.UserRole
@ -42,104 +45,112 @@ fun CinemaView(
) {
val cinemaUiState = viewModel.cinemaUiState
val user = LiveStore.user.observeAsState()
LaunchedEffect(Unit) {
viewModel.refreshState()
}
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxSize(),
) {
val cinema: Cinema? = cinemaUiState.cinemaWithSessions?.cinema
if (cinema != null) {
Box(
when (viewModel.apiStatus) {
ApiStatus.DONE -> {
Column(
modifier = Modifier
.background(
color = MaterialTheme.colorScheme.secondary,
shape = RoundedCornerShape(16.dp)
)
.padding(16.dp)
.fillMaxSize(),
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.background(color = MaterialTheme.colorScheme.secondary),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
val cinema: Cinema? = cinemaUiState.cinemaWithSessions?.cinema
if (cinema != null) {
Box(
modifier = Modifier
.background(
color = MaterialTheme.colorScheme.secondary,
shape = RoundedCornerShape(16.dp)
)
) {
Text(
text = "${cinema.name}, ${cinema.year}",
style = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
color = MaterialTheme.colorScheme.onSecondary
),
modifier = Modifier
.padding(bottom = 8.dp)
)
}
if (cinema.image != null)
Image(
bitmap = BitmapFactory.decodeByteArray(
cinema.image,
0,
cinema.image.size
).asImageBitmap(),
contentDescription = null,
Column(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.padding(4.dp)
)
.padding(16.dp)
.background(color = MaterialTheme.colorScheme.secondary),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
text = "${cinema.name}, ${cinema.year}",
style = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
color = MaterialTheme.colorScheme.onSecondary
),
modifier = Modifier
.padding(bottom = 8.dp)
)
}
Text(
text = cinema.description,
color = MaterialTheme.colorScheme.onSecondary
)
}
}
}
if (cinema.image != null)
Image(
bitmap = BitmapFactory.decodeByteArray(
cinema.image,
0,
cinema.image.size
).asImageBitmap(),
contentDescription = null,
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.padding(4.dp)
)
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Сеансы",
style = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
color = MaterialTheme.colorScheme.onBackground
),
modifier = Modifier
.weight(1f) // Занимает доступное пространство
.padding(top = 8.dp, bottom = 8.dp)
)
if (user.value?.role == UserRole.ADMIN) {
IconButton(
onClick = {
val route = Screen.SessionEdit.route.replace("{id}", 0.toString())
.replace(
"{cinemaId}",
cinemaUiState.cinemaWithSessions?.cinema?.uid.toString()
Text(
text = cinema.description,
color = MaterialTheme.colorScheme.onSecondary
)
navController.navigate(route)
}
}
}
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Filled.Add,
contentDescription = "Добавить сеанс",
Text(
text = "Сеансы",
style = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
color = MaterialTheme.colorScheme.onBackground
),
modifier = Modifier
.weight(1f) // Занимает доступное пространство
.padding(top = 8.dp, bottom = 8.dp)
)
if (user.value?.role == UserRole.ADMIN) {
IconButton(
onClick = {
val route = Screen.SessionEdit.route.replace("{id}", 0.toString())
.replace(
"{cinemaId}",
cinemaUiState.cinemaWithSessions?.cinema?.uid.toString()
)
navController.navigate(route)
}
) {
Icon(
imageVector = Icons.Filled.Add,
contentDescription = "Добавить сеанс",
)
}
}
}
if (cinemaUiState.cinemaWithSessions != null) {
SessionList(viewModel, navController)
}
}
}
if (cinemaUiState.cinemaWithSessions != null) {
SessionList(viewModel, navController)
}
ApiStatus.LOADING -> LoadingPlaceholder()
else -> ErrorPlaceholder(
message = viewModel.apiError,
onBack = { navController.popBackStack() }
)
}
}

View File

@ -4,13 +4,13 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import com.example.myapplication.composeui.MyViewModel
import com.example.myapplication.database.entities.model.CinemaWithSessions
import com.example.myapplication.database.entities.repository.CinemaRepository
class CinemaViewModel(
savedStateHandle: SavedStateHandle, private val cinemaRepository: CinemaRepository
) : ViewModel() {
) : MyViewModel() {
private val cinemaUid: Int = checkNotNull(savedStateHandle["id"])
var cinemaUiState by mutableStateOf(CinemaUiState())
@ -18,7 +18,11 @@ class CinemaViewModel(
suspend fun refreshState() {
if (cinemaUid > 0) {
cinemaUiState = CinemaUiState(cinemaRepository.getCinema(cinemaUid))
runInScope(actionSuccess = {
cinemaUiState = CinemaUiState(cinemaRepository.getCinema(cinemaUid))
}, actionError = {
cinemaUiState = CinemaUiState()
})
}
}
}

View File

@ -15,8 +15,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
@ -26,7 +24,6 @@ import androidx.navigation.NavController
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemContentType
import androidx.paging.compose.itemKey
import com.example.myapplication.LiveStore
import com.example.myapplication.composeui.navigation.Screen
import com.example.myapplication.ui.theme.PmudemoTheme
import org.threeten.bp.format.DateTimeFormatter
@ -36,11 +33,8 @@ fun OrderList(
navController: NavController?,
viewModel: OrderListViewModel = viewModel(factory = AppViewModelProvider.Factory)
) {
val user = LiveStore.user.observeAsState()
val ordersUiState = viewModel.orderListUiState.collectAsLazyPagingItems()
LaunchedEffect(user.value?.uid) {
viewModel.refreshState(user.value?.uid ?: 0)
}
LazyColumn(
modifier = Modifier
.fillMaxSize()

View File

@ -1,18 +1,13 @@
package com.example.myapplication.database.entities.composeui
import androidx.lifecycle.ViewModel
import androidx.paging.PagingData
import com.example.myapplication.composeui.MyViewModel
import com.example.myapplication.database.entities.model.Order
import com.example.myapplication.database.entities.repository.OrderRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
class OrderListViewModel(
private val orderRepository: OrderRepository
) : ViewModel() {
var orderListUiState: Flow<PagingData<Order>> = emptyFlow()
fun refreshState(userId: Int = 0) {
orderListUiState = orderRepository.getAllOrders(userId)
}
orderRepository: OrderRepository
) : MyViewModel() {
var orderListUiState: Flow<PagingData<Order>> = orderRepository.getAllOrders()
}

View File

@ -1,6 +1,5 @@
package com.example.myapplication.database.entities.composeui
import android.content.res.Configuration
import android.graphics.BitmapFactory
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@ -15,100 +14,98 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.myapplication.ui.theme.PmudemoTheme
import androidx.navigation.NavController
import com.example.myapplication.api.ApiStatus
import com.example.myapplication.composeui.ErrorPlaceholder
import com.example.myapplication.composeui.LoadingPlaceholder
import org.threeten.bp.format.DateTimeFormatter
@Composable
fun OrderView(
id: Int,
navController: NavController,
viewModel: OrderViewModel = viewModel(factory = AppViewModelProvider.Factory)
) {
val coroutineScope = rememberCoroutineScope()
val orderUiState by viewModel.orderUiState.collectAsState()
LazyColumn(
modifier = Modifier
.padding(10.dp)
) {
items(orderUiState.sessionList) { session ->
val count = remember { mutableStateOf(session.count) }
val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")
val formattedDate = dateFormatter.format(session.dateTime)
val orderUiState = viewModel.orderUiState
Text(
text = formattedDate,
color = MaterialTheme.colorScheme.onBackground,
)
Box(
LaunchedEffect(Unit) {
viewModel.refreshState()
}
when (viewModel.apiStatus) {
ApiStatus.DONE -> {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp)
.clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colorScheme.secondary)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
if (session.cinema.image != null)
Image(
bitmap = BitmapFactory.decodeByteArray(
session.cinema.image,
0,
session.cinema.image.size
).asImageBitmap(),
contentDescription = null,
modifier = Modifier
.size(90.dp)
.padding(4.dp)
)
items(orderUiState.sessionList) { session ->
val count = remember { mutableIntStateOf(session.count) }
val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")
val formattedDate = dateFormatter.format(session.dateTime)
Column(
Text(
text = formattedDate,
color = MaterialTheme.colorScheme.onBackground,
)
Box(
modifier = Modifier
.weight(1f)
.padding(start = 8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
.fillMaxWidth()
.padding(10.dp)
.clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colorScheme.secondary)
) {
Text(
text = "${session.cinema.name}, ${session.cinema.year}\n" +
"Цена: ${session.frozenPrice}\n" +
"Количество: ${count.value}",
color = MaterialTheme.colorScheme.onSecondary
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
if (session.cinema.image != null)
Image(
bitmap = BitmapFactory.decodeByteArray(
session.cinema.image,
0,
session.cinema.image.size
).asImageBitmap(),
contentDescription = null,
modifier = Modifier
.size(90.dp)
.padding(4.dp)
)
Column(
modifier = Modifier
.weight(1f)
.padding(start = 8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = "${session.cinema.name}, ${session.cinema.year}\n" +
"Цена: ${session.frozenPrice}\n" +
"Количество: ${count.intValue}",
color = MaterialTheme.colorScheme.onSecondary
)
}
}
}
}
}
}
}
}
@Preview(name = "Light Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun OrderViewPreview() {
PmudemoTheme {
Surface(
color = MaterialTheme.colorScheme.background
) {
OrderView(id = 1)
}
ApiStatus.LOADING -> LoadingPlaceholder()
else -> ErrorPlaceholder(
message = viewModel.apiError,
onBack = { navController.popBackStack() }
)
}
}

View File

@ -1,31 +1,29 @@
package com.example.myapplication.database.entities.composeui
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.myapplication.database.AppContainer
import com.example.myapplication.composeui.MyViewModel
import com.example.myapplication.database.entities.model.SessionFromOrder
import com.example.myapplication.database.entities.repository.OrderRepository
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
class OrderViewModel(
savedStateHandle: SavedStateHandle,
private val orderRepository: OrderRepository
) : ViewModel() {
) : MyViewModel() {
private val orderUid: Int = checkNotNull(savedStateHandle["id"])
val orderUiState: StateFlow<OrderUiState> =
flow { emit(orderRepository.getOrder(orderUid)) }.map {
OrderUiState(it)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = AppContainer.TIMEOUT),
initialValue = OrderUiState()
)
var orderUiState by mutableStateOf(OrderUiState())
private set
suspend fun refreshState() {
runInScope(actionSuccess = {
orderUiState = OrderUiState(orderRepository.getOrder(orderUid))
}, actionError = {
orderUiState = OrderUiState()
})
}
}
data class OrderUiState(val sessionList: List<SessionFromOrder> = listOf())

View File

@ -113,8 +113,7 @@ fun SessionList(
IconButton(
onClick = {
coroutineScope.launch {
if (session.availableCount != 0)
viewModel.addSessionInCart(sessionId = session.uid)
viewModel.addSessionInCart(sessionId = session.uid)
}
},
) {

View File

@ -1,7 +1,7 @@
package com.example.myapplication.database.entities.composeui
import androidx.lifecycle.ViewModel
import com.example.myapplication.LiveStore
import com.example.myapplication.composeui.MyViewModel
import com.example.myapplication.database.entities.model.Session
import com.example.myapplication.database.entities.model.SessionFromCinema
import com.example.myapplication.database.entities.model.UserSessionCrossRef
@ -11,25 +11,31 @@ import com.example.myapplication.database.entities.repository.UserSessionReposit
class SessionListViewModel(
private val sessionRepository: SessionRepository,
private val userSessionRepository: UserSessionRepository
) : ViewModel() {
) : MyViewModel() {
suspend fun deleteSession(session: SessionFromCinema) {
sessionRepository.deleteSession(
Session(
uid = session.uid,
dateTime = session.dateTime,
price = session.price,
maxCount = 0,
cinemaId = 0
runInScope(actionSuccess = {
sessionRepository.deleteSession(
Session(
uid = session.uid,
dateTime = session.dateTime,
price = session.price,
maxCount = 0,
cinemaId = 0
)
)
)
})
}
suspend fun addSessionInCart(sessionId: Int, count: Int = 1) {
try {
val userId: Int = LiveStore.user.value?.uid ?: return
userSessionRepository.insertUserSession(UserSessionCrossRef(userId, sessionId, count))
} catch (_: Exception) {
}
val userId: Int = LiveStore.user.value?.uid ?: return
runInScope(actionSuccess = {
userSessionRepository.insertUserSession(
UserSessionCrossRef(
userId,
sessionId,
count
)
)
})
}
}

View File

@ -18,6 +18,7 @@ import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
@ -32,7 +33,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.example.myapplication.LiveStore
import com.example.myapplication.datastore.DataStoreManager
import kotlinx.coroutines.launch
@ -41,15 +41,21 @@ import kotlinx.coroutines.launch
fun UserProfile(
isDarkTheme: MutableState<Boolean>,
dataStoreManager: DataStoreManager,
navController: NavController,
viewModel: UserProfileViewModel = viewModel(factory = AppViewModelProvider.Factory),
) {
var isRegistration by remember { mutableStateOf(false) }
val coroutine = rememberCoroutineScope()
val errorStringId: Int? = viewModel.userUiState.errorId
val errorMessage = if (errorStringId == null) "" else stringResource(errorStringId)
val errorMessage =
if (errorStringId == null || errorStringId == 0) "" else stringResource(errorStringId)
val user = LiveStore.user.observeAsState()
LaunchedEffect(errorStringId) {
if (errorStringId == 0) {
isRegistration = false
}
}
LazyColumn {
item {
if (user.value != null) {
@ -70,7 +76,7 @@ fun UserProfile(
},
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.padding(start = 2.dp, top = 10.dp)
) { Text("Выход") }
}
} else {
@ -151,10 +157,10 @@ fun UserProfile(
if (isRegistration) {
Button(
onClick = { coroutine.launch { isRegistration = !viewModel.signUp() } },
onClick = { coroutine.launch { viewModel.signUp() } },
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.padding(start = 2.dp, top = 8.dp)
) {
Text("Регистрация")
}
@ -171,14 +177,12 @@ fun UserProfile(
Button(
onClick = {
coroutine.launch {
if (viewModel.signIn(dataStoreManager)) {
navController.popBackStack()
}
viewModel.signIn(dataStoreManager)
}
},
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.padding(start = 2.dp, top = 8.dp)
) {
Text("Вход")
}

View File

@ -1,17 +1,20 @@
package com.example.myapplication.database.entities.composeui
import android.util.Log
import android.widget.Toast
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import com.example.myapplication.MainComposeActivity
import com.example.myapplication.R
import com.example.myapplication.composeui.MyViewModel
import com.example.myapplication.database.entities.model.User
import com.example.myapplication.database.entities.repository.UserRepository
import com.example.myapplication.datastore.DataStoreManager
class UserProfileViewModel(
private val userRepository: UserRepository
) : ViewModel() {
) : MyViewModel() {
var userUiState by mutableStateOf(UserUiState())
private set
@ -22,43 +25,63 @@ class UserProfileViewModel(
)
}
suspend fun signIn(dataStoreManager: DataStoreManager): Boolean {
suspend fun signIn(dataStoreManager: DataStoreManager) {
userUiState.details.passwordConfirm = userUiState.details.password
var errorId: Int? = validateInput(userUiState.details)
if (errorId == null) {
val overlap = userRepository.getUser(userUiState.details.login)
if (overlap == null || userUiState.details.password != overlap.password) {
errorId = R.string.err_04
}
}
userUiState = UserUiState(
details = userUiState.details,
errorId = errorId
)
if (errorId == null) {
dataStoreManager.setLogin(userUiState.details.login)
return true
}
return false
runInScope(
actionSuccess = {
if (errorId == null) {
val overlap: User? = userRepository.getUser(userUiState.details.login)
if (overlap == null || userUiState.details.password != overlap.password) {
errorId = R.string.err_04
} else {
dataStoreManager.setLogin(userUiState.details.login)
Log.d("UserProfileViewModel", "sign in success")
}
}
userUiState = UserUiState(
details = userUiState.details,
errorId = errorId
)
}, actionError = {
errorId = R.string.err_06
userUiState = UserUiState(
details = userUiState.details,
errorId = errorId
)
})
}
suspend fun signUp(): Boolean {
suspend fun signUp() {
var errorId: Int? = validateInput(userUiState.details)
if (errorId == null) {
val overlap = userRepository.getUser(userUiState.details.login)
if (overlap != null) {
errorId = R.string.err_03
runInScope(actionSuccess = {
if (errorId == null) {
val overlap = userRepository.getUser(userUiState.details.login)
if (overlap != null) {
errorId = R.string.err_03
}
}
}
userUiState = UserUiState(
details = userUiState.details,
errorId = errorId
)
if (errorId == null) {
userRepository.insertUser(userUiState.details.toUser())
return true
}
return false
if (errorId == null) {
userRepository.insertUser(userUiState.details.toUser())
val toast = Toast.makeText(
MainComposeActivity.appContext,
"Вы зарегистрированы",
Toast.LENGTH_SHORT
)
toast.show()
errorId = 0
}
userUiState = UserUiState(
details = userUiState.details,
errorId = errorId
)
}, actionError = {
errorId = R.string.err_06
userUiState = UserUiState(
details = userUiState.details,
errorId = errorId
)
})
}
private fun validateInput(details: UserDetails = userUiState.details): Int? {

View File

@ -22,6 +22,9 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.example.myapplication.R
import com.example.myapplication.api.ApiStatus
import com.example.myapplication.composeui.ErrorPlaceholder
import com.example.myapplication.composeui.LoadingPlaceholder
import com.example.myapplication.database.entities.composeui.AppViewModelProvider
import com.example.myapplication.ui.theme.PmudemoTheme
import kotlinx.coroutines.launch
@ -32,17 +35,25 @@ fun CinemaEdit(
viewModel: CinemaEditViewModel = viewModel(factory = AppViewModelProvider.Factory),
) {
val coroutineScope = rememberCoroutineScope()
CinemaEdit(
cinemaUiState = viewModel.cinemaUiState,
onClick = {
coroutineScope.launch {
viewModel.saveCinema()
navController.popBackStack()
}
},
onUpdate = viewModel::updateUiState,
)
when (viewModel.apiStatus) {
ApiStatus.DONE -> {
CinemaEdit(
cinemaUiState = viewModel.cinemaUiState,
onClick = {
coroutineScope.launch {
viewModel.saveCinema()
navController.popBackStack()
}
},
onUpdate = viewModel::updateUiState,
)
}
ApiStatus.LOADING -> LoadingPlaceholder()
else -> ErrorPlaceholder(
message = viewModel.apiError,
onBack = { navController.popBackStack() }
)
}
}
@Composable
@ -100,7 +111,6 @@ private fun CinemaEdit(
Button(
onClick = onClick,
enabled = cinemaUiState.isEntryValid,
shape = MaterialTheme.shapes.small,
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(R.string.Save_button))

View File

@ -9,7 +9,6 @@ import androidx.compose.material3.Button
import androidx.compose.material3.DatePicker
import androidx.compose.material3.DisplayMode
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TimePicker
@ -24,6 +23,9 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.example.myapplication.R
import com.example.myapplication.api.ApiStatus
import com.example.myapplication.composeui.ErrorPlaceholder
import com.example.myapplication.composeui.LoadingPlaceholder
import com.example.myapplication.database.entities.composeui.AppViewModelProvider
import kotlinx.coroutines.launch
import org.threeten.bp.Instant
@ -39,16 +41,26 @@ fun SessionEdit(
viewModel: SessionEditViewModel = viewModel(factory = AppViewModelProvider.Factory),
) {
val coroutineScope = rememberCoroutineScope()
SessionEdit(
sessionUiState = viewModel.sessionUiState,
onClick = {
coroutineScope.launch {
viewModel.saveSession()
navController.popBackStack()
}
},
onUpdate = viewModel::updateUiState
)
when (viewModel.apiStatus) {
ApiStatus.DONE -> {
SessionEdit(
sessionUiState = viewModel.sessionUiState,
onClick = {
coroutineScope.launch {
viewModel.saveSession()
navController.popBackStack()
}
},
onUpdate = viewModel::updateUiState
)
}
ApiStatus.LOADING -> LoadingPlaceholder()
else -> ErrorPlaceholder(
message = viewModel.apiError,
onBack = { navController.popBackStack() }
)
}
}
fun Long.toLocalDate(): org.threeten.bp.LocalDate {
@ -140,7 +152,6 @@ private fun SessionEdit(
Button(
onClick = onClick,
enabled = sessionUiState.isEntryValid,
shape = MaterialTheme.shapes.small,
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(R.string.Save_button))

View File

@ -4,6 +4,7 @@ import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.example.myapplication.database.entities.model.Cinema
@ -14,6 +15,7 @@ import kotlinx.coroutines.flow.Flow
interface CinemaDao {
@Query("select * from cinemas order by name")
fun getAll(): PagingSource<Int, Cinema>
@Query("select * from cinemas where cinemas.name like :name order by name collate nocase asc")
fun getAll(name: String): PagingSource<Int, Cinema>
@ -27,7 +29,7 @@ interface CinemaDao {
)
fun getByUid(cinemaId: Int?): Flow<Map<Cinema, List<SessionFromCinema>>>
@Insert
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(vararg cinema: Cinema)
@Update

View File

@ -4,6 +4,7 @@ import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.example.myapplication.database.entities.model.Order
@ -23,7 +24,7 @@ interface OrderDao {
)
fun getByUid(orderId: Int?): List<SessionFromOrder>
@Insert
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(vararg order: Order): List<Long>
@Update

View File

@ -3,6 +3,7 @@ package com.example.myapplication.database.entities.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.example.myapplication.database.entities.model.Session
@ -12,7 +13,7 @@ interface SessionDao {
@Query("select * from sessions where sessions.uid = :uid")
suspend fun getByUid(uid: Int): Session
@Insert
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(vararg session: Session)
@Update

View File

@ -3,6 +3,7 @@ package com.example.myapplication.database.entities.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.example.myapplication.database.entities.model.SessionFromCart
@ -27,7 +28,7 @@ interface UserDao {
)
suspend fun getCartByUid(userId: Int): List<SessionFromCart>
@Insert
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(vararg user: User)
@Update

View File

@ -17,7 +17,7 @@ interface UserSessionCrossRefDao {
suspend fun update(userSessionCrossRef: UserSessionCrossRef)
@Delete
suspend fun delete(userSessionCrossRef: UserSessionCrossRef)
suspend fun delete(vararg userSessionCrossRef: UserSessionCrossRef)
@Query("DELETE FROM users_sessions where users_sessions.user_id = :userId")
suspend fun deleteByUserUid(userId: Int)

View File

@ -18,7 +18,7 @@ class OfflineCinemaRepository(private val cinemaDao: CinemaDao) : CinemaReposito
pageSize = AppContainer.LIMIT,
enablePlaceholders = false
),
pagingSourceFactory = cinemaDao::getAll
pagingSourceFactory = { cinemaDao.getAll(name) }
).flow

View File

@ -4,6 +4,7 @@ import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.PagingSource
import com.example.myapplication.LiveStore
import com.example.myapplication.database.AppContainer
import com.example.myapplication.database.entities.dao.OrderDao
import com.example.myapplication.database.entities.model.Order
@ -11,19 +12,21 @@ import com.example.myapplication.database.entities.model.SessionFromOrder
import kotlinx.coroutines.flow.Flow
class OfflineOrderRepository(private val orderDao: OrderDao) : OrderRepository {
override fun getAllOrders(userId: Int): Flow<PagingData<Order>> = Pager(
override fun getAllOrders(): Flow<PagingData<Order>> = Pager(
config = PagingConfig(
pageSize = AppContainer.LIMIT,
enablePlaceholders = false
),
pagingSourceFactory = { orderDao.getAll(userId) }
pagingSourceFactory = { orderDao.getAll(LiveStore.user.value?.uid ?: 0) }
).flow
override suspend fun getOrder(uid: Int): List<SessionFromOrder> = orderDao.getByUid(uid)
override suspend fun insertOrder(order: Order): Long = orderDao.insert(order).first()
fun getAllOrdersPagingSource(userId: Int?): PagingSource<Int, Order> = orderDao.getAll(userId)
fun getAllOrdersPagingSource(): PagingSource<Int, Order> {
return orderDao.getAll(LiveStore.user.value?.uid ?: 0)
}
suspend fun clearOrders() = orderDao.deleteAll()

View File

@ -16,5 +16,8 @@ class OfflineUserSessionRepository(private val userSessionDao: UserSessionCrossR
override suspend fun deleteUserSessions(userId: Int) = userSessionDao.deleteByUserUid(userId)
override suspend fun deleteUserSessions(userSessionCrossRefs: List<UserSessionCrossRef>) =
userSessionDao.delete(*userSessionCrossRefs.toTypedArray())
suspend fun deleteSessionsByUid(sessionId: Int) = userSessionDao.deleteBySessionUid(sessionId)
}

View File

@ -6,7 +6,7 @@ import com.example.myapplication.database.entities.model.SessionFromOrder
import kotlinx.coroutines.flow.Flow
interface OrderRepository {
fun getAllOrders(userId: Int): Flow<PagingData<Order>>
fun getAllOrders(): Flow<PagingData<Order>>
suspend fun getOrder(uid: Int): List<SessionFromOrder>
suspend fun insertOrder(order: Order): Long
}

View File

@ -7,4 +7,5 @@ interface UserSessionRepository {
suspend fun updateUserSession(userSessionCrossRef: UserSessionCrossRef)
suspend fun deleteUserSession(userSessionCrossRef: UserSessionCrossRef)
suspend fun deleteUserSessions(userId: Int)
suspend fun deleteUserSessions(userSessionCrossRefs: List<UserSessionCrossRef>)
}

View File

@ -25,6 +25,7 @@
<string name="err_03">Логин занят</string>
<string name="err_04">Неверный логин или пароль</string>
<string name="err_05">Не совпадают пароли</string>
<string name="err_06">Ошибка сети</string>
<string name="back">Назад</string>
<string name="loading">Загрузка…</string>
</resources>

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@ module.exports = (req, res, next) => {
try {
const { startDate, endDate } = req.query;
const { sessions, orders } = require('./data.json');
const { cinemas, sessions, orders } = require('./data.json');
const start = new Date(startDate);
const end = new Date(endDate);
@ -42,8 +42,9 @@ module.exports = (req, res, next) => {
};
}, { totalTicketsSold: 0, revenue: 0 });
const cinema = cinemas.find(cinema => cinema.id === session.cinemaId)
return {
cinema_name: relevantOrders[0].sessions[0].cinema.name,
cinema_name: cinema ? cinema.name : "Неизвестно",
current_ticket_date_time: session.dateTime,
current_ticket_price: session.price,
max_ticket_quantity: session.maxCount,