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 package com.example.myapplication
import android.content.Context
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
@ -20,6 +21,7 @@ class MainComposeActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
application.deleteDatabase("pmy-db") application.deleteDatabase("pmy-db")
appContext = applicationContext
setContent { setContent {
PmudemoTheme(darkTheme = isDarkTheme.value) { PmudemoTheme(darkTheme = isDarkTheme.value) {
LaunchedEffect(key1 = true) { 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, @Query("userId") userId: Int,
): List<UserSessionWithSessionRemote> ): List<UserSessionWithSessionRemote>
@GET("userssessions")
suspend fun getUserSessions(
@Query("userId") userId: Int,
): List<UserSessionRemote>
@GET("userssessions?_expand=session") @GET("userssessions?_expand=session")
suspend fun getUsersSessions(): List<UserSessionWithSessionRemote> suspend fun getUsersSessions(): List<UserSessionWithSessionRemote>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,6 @@
package com.example.myapplication.api.user package com.example.myapplication.api.user
import com.example.myapplication.api.session.SessionRemote 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 import kotlinx.serialization.Serializable
@Serializable @Serializable
@ -12,17 +10,4 @@ data class UserSessionWithSessionRemote(
val sessionId: Int = 0, val sessionId: Int = 0,
val count: Int = 0, val count: Int = 0,
val session: SessionRemote, 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( val userSessionRemote = service.getUserSession(
userSessionCrossRef.userId, userSessionCrossRef.userId,
userSessionCrossRef.sessionId userSessionCrossRef.sessionId
).first() ).firstOrNull() ?: return
service.deleteUserSession(userSessionRemote.id) service.deleteUserSession(userSessionRemote.id)
dbUserSessionRepository.deleteUserSession(userSessionCrossRef) dbUserSessionRepository.deleteUserSession(userSessionCrossRef)
} }
override suspend fun deleteUserSessions(userId: Int) { override suspend fun deleteUserSessions(userId: Int) {
val cart = service.getUserCart(userId) val cart = service.getUserSessions(userId)
cart.forEach { cart.forEach {
service.deleteUserSession(it.id) service.deleteUserSession(it.id)
} }
dbUserSessionRepository.deleteUserSessions(userId) 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) viewModel: AuthenticatorViewModel = viewModel(factory = AppViewModelProvider.Factory)
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val login = dataStoreManager.getLogin().collectAsState(initial = "").value val login = dataStoreManager.getLogin().collectAsState(initial = "").value
LiveStore.user.value = viewModel.authUiState.user
fun synchronize() { fun synchronize() {
scope.launch { scope.launch {
if (login == "") { if (login == "") {
LiveStore.user.value = null LiveStore.user.value = null
return@launch return@launch
} }
val overlap = viewModel.findUserByLogin(login) viewModel.findUserByLogin(login)
if (overlap == null) {
dataStoreManager.setLogin("")
return@launch
}
LiveStore.user.value = overlap
} }
} }

View File

@ -1,18 +1,27 @@
package com.example.myapplication.composeui 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.model.User
import com.example.myapplication.database.entities.repository.UserRepository import com.example.myapplication.database.entities.repository.UserRepository
class AuthenticatorViewModel( class AuthenticatorViewModel(
private val userRepository: UserRepository private val userRepository: UserRepository
) : MyViewModel() { ) : MyViewModel() {
suspend fun findUserByLogin(login: String): User? { var authUiState by mutableStateOf(AuthenticatorUiState())
var user: User? = null private set
suspend fun findUserByLogin(login: String) {
runInScope( runInScope(
actionSuccess = { 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 package com.example.myapplication.composeui
import android.content.res.Configuration
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background 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.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn 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.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.Button 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.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.rememberDismissState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.livedata.observeAsState 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.asImageBitmap
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.example.myapplication.LiveStore import com.example.myapplication.LiveStore
import com.example.myapplication.R 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.composeui.AppViewModelProvider
import com.example.myapplication.database.entities.model.Session import com.example.myapplication.database.entities.model.Session
import com.example.myapplication.database.entities.model.SessionFromCart import com.example.myapplication.database.entities.model.SessionFromCart
import com.example.myapplication.database.entities.model.UserRole import com.example.myapplication.database.entities.model.UserRole
import com.example.myapplication.ui.theme.PmudemoTheme
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.threeten.bp.format.DateTimeFormatter import org.threeten.bp.format.DateTimeFormatter
@Composable @Composable
fun Cart( fun Cart(
navController: NavController,
viewModel: CartViewModel = viewModel(factory = AppViewModelProvider.Factory) viewModel: CartViewModel = viewModel(factory = AppViewModelProvider.Factory)
) { ) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
@ -60,65 +58,66 @@ fun Cart(
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.refreshState() viewModel.refreshState()
} }
when (viewModel.apiStatus) {
Cart( ApiStatus.DONE -> {
cartUiState = cartUiState, Cart(
modifier = Modifier cartUiState = cartUiState,
.padding(all = 10.dp), modifier = Modifier
onSwipe = { session: SessionFromCart -> .padding(all = 10.dp),
coroutineScope.launch { onChangeCount = { session: SessionFromCart, count: Int ->
viewModel.removeFromCart( coroutineScope.launch {
session = Session( viewModel.updateFromCart(
uid = session.uid, session = Session(
dateTime = session.dateTime, uid = session.uid,
price = session.price, dateTime = session.dateTime,
maxCount = 0, price = session.price,
cinemaId = session.cinemaId maxCount = 0,
) cinemaId = session.cinemaId
) ), count = count, availableCount = session.availableCount
} )
}, }
onChangeCount = { session: SessionFromCart, count: Int -> },
coroutineScope.launch { onAddToOrder = { sessions: List<SessionFromCart> ->
viewModel.updateFromCart( coroutineScope.launch {
session = Session( viewModel.addToOrder(sessions = sessions)
uid = session.uid, }
dateTime = session.dateTime, },
price = session.price, onDelete = { session: SessionFromCart ->
maxCount = 0, coroutineScope.launch {
cinemaId = session.cinemaId viewModel.removeFromCart(
), count = count, availableCount = session.availableCount session = Session(
) uid = session.uid,
} dateTime = session.dateTime,
}, price = session.price,
onAddToOrder = { sessions: List<SessionFromCart> -> maxCount = 0,
coroutineScope.launch { cinemaId = session.cinemaId
viewModel.addToOrder(sessions = sessions) )
} )
}
}
)
} }
)
ApiStatus.LOADING -> LoadingPlaceholder()
else -> ErrorPlaceholder(
message = viewModel.apiError,
onBack = { navController.navigate(Screen.Report.route) }
)
}
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun Cart( private fun Cart(
cartUiState: CartUiState, cartUiState: CartUiState,
modifier: Modifier, modifier: Modifier,
onSwipe: (SessionFromCart) -> Unit,
onChangeCount: (SessionFromCart, Int) -> Unit, onChangeCount: (SessionFromCart, Int) -> Unit,
onAddToOrder: (List<SessionFromCart>) -> Unit onAddToOrder: (List<SessionFromCart>) -> Unit,
onDelete: (SessionFromCart) -> Unit
) { ) {
LazyColumn( LazyColumn(
modifier = modifier modifier = modifier
) { ) {
items(cartUiState.sessionList, key = { it.uid.toString() }) { session -> 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( SessionListItem(
session = session, session = session,
modifier = Modifier modifier = Modifier
@ -126,9 +125,13 @@ private fun Cart(
.padding(10.dp) .padding(10.dp)
.clip(RoundedCornerShape(16.dp)) .clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colorScheme.secondary), .background(MaterialTheme.colorScheme.secondary),
onChangeCount = onChangeCount onChangeCount = onChangeCount,
onDelete = onDelete,
) )
} }
item {
Spacer(modifier = Modifier.height(48.dp))
}
} }
val user = LiveStore.user.observeAsState() val user = LiveStore.user.observeAsState()
if (user.value?.role == UserRole.USER) { if (user.value?.role == UserRole.USER) {
@ -138,7 +141,7 @@ private fun Cart(
Button( Button(
onClick = { onAddToOrder(cartUiState.sessionList) }, onClick = { onAddToOrder(cartUiState.sessionList) },
modifier = Modifier modifier = Modifier
.padding(16.dp) .padding(6.dp)
.fillMaxWidth() .fillMaxWidth()
) { Text("Купить") } ) { Text("Купить") }
} }
@ -150,27 +153,26 @@ private fun SessionListItem(
session: SessionFromCart, session: SessionFromCart,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onChangeCount: (SessionFromCart, Int) -> Unit, onChangeCount: (SessionFromCart, Int) -> Unit,
onDelete: (SessionFromCart) -> Unit
) { ) {
//var currentCount by remember { mutableIntStateOf(session.count) }
val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm") val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")
val formattedDate = dateFormatter.format(session.dateTime) val formattedDate = dateFormatter.format(session.dateTime)
Column { Text(
Text( text = formattedDate,
text = formattedDate, color = MaterialTheme.colorScheme.onBackground,
color = MaterialTheme.colorScheme.onBackground, )
) Column(modifier = modifier.fillMaxWidth()) {
Box( Box(
modifier = modifier modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier.fillMaxWidth(),
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
if (session.cinema.image != null) if (session.cinema.image != null) {
Image( Image(
bitmap = BitmapFactory.decodeByteArray( bitmap = BitmapFactory.decodeByteArray(
session.cinema.image, session.cinema.image,
@ -182,6 +184,7 @@ private fun SessionListItem(
.size(90.dp) .size(90.dp)
.padding(4.dp) .padding(4.dp)
) )
}
Column( Column(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
@ -190,68 +193,66 @@ private fun SessionListItem(
Text( Text(
text = "${session.cinema.name}, ${session.cinema.year}\n" + text = "${session.cinema.name}, ${session.cinema.year}\n" +
"Цена: ${session.price}\n" + "Цена: ${session.price}\n" +
"${session.count}/${session.availableCount}", if (session.availableCount == 0) "Недоступно" else "${session.count}/${session.availableCount}",
color = MaterialTheme.colorScheme.onSecondary color = MaterialTheme.colorScheme.onSecondary
) )
} }
}
}
Box( Row(
modifier = Modifier modifier = Modifier
.background( .background(color = MaterialTheme.colorScheme.primary)
color = MaterialTheme.colorScheme.background, .fillMaxWidth(),
shape = RoundedCornerShape(10.dp) 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( Icon(
verticalAlignment = Alignment.CenterVertically imageVector = ImageVector.vectorResource(id = R.drawable.minus),
) { contentDescription = "Уменьшить",
IconButton( tint = MaterialTheme.colorScheme.onPrimary
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)
)
}
}
} }
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.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.room.Transaction
import com.example.myapplication.LiveStore import com.example.myapplication.LiveStore
import com.example.myapplication.database.entities.model.Order import com.example.myapplication.database.entities.model.Order
import com.example.myapplication.database.entities.model.OrderSessionCrossRef import com.example.myapplication.database.entities.model.OrderSessionCrossRef
@ -21,56 +21,93 @@ class CartViewModel(
private val orderRepository: OrderRepository, private val orderRepository: OrderRepository,
private val orderSessionRepository: OrderSessionRepository, private val orderSessionRepository: OrderSessionRepository,
private val userRepository: UserRepository, private val userRepository: UserRepository,
) : ViewModel() { ) : MyViewModel() {
private var isLoading: Boolean = false private var isLoading: Boolean = false
var cartUiState by mutableStateOf(CartUiState()) var cartUiState by mutableStateOf(CartUiState())
private set private set
suspend fun refreshState() { suspend fun refreshState(needLoadingScreen: Boolean = true) {
val userId: Int = LiveStore.user.value?.uid ?: 0 val userId: Int = LiveStore.user.value?.uid ?: return
val cart = userRepository.getCartByUser(userId) runInScope(
cartUiState = CartUiState(cart) actionSuccess = {
cartUiState = CartUiState(userRepository.getCartByUser(userId))
}, actionError = {
cartUiState = CartUiState()
},
needLoadingScreen = needLoadingScreen
)
} }
@Transaction
suspend fun addToOrder(sessions: List<SessionFromCart>) { suspend fun addToOrder(sessions: List<SessionFromCart>) {
if (isLoading) if (isLoading)
return return
isLoading = true isLoading = true
val userId: Int = LiveStore.user.value?.uid ?: return val userId: Int = LiveStore.user.value?.uid ?: return
if (sessions.isEmpty()) val cart = sessions.filter { it.availableCount != 0 }
if (cart.isEmpty())
return return
val orderId = orderRepository.insertOrder(Order(0, userId, LocalDateTime.now())) runInScope(
sessions.forEach { session -> actionSuccess = {
orderSessionRepository.insertOrderSession( val orderId = orderRepository.insertOrder(Order(0, userId, LocalDateTime.now()))
OrderSessionCrossRef( cart.forEach { session ->
orderId.toInt(), if (session.availableCount != 0) {
session.uid, orderSessionRepository.insertOrderSession(
session.price, OrderSessionCrossRef(
session.count orderId.toInt(),
) session.uid,
) session.price,
} session.count
userSessionRepository.deleteUserSessions(userId) )
refreshState() )
}
}
userSessionRepository.deleteUserSessions(cart.map {
UserSessionCrossRef(userId, it.uid, it.count)
})
refreshState()
}
)
} }
suspend fun removeFromCart(session: Session, count: Int = 0) { suspend fun removeFromCart(session: Session, count: Int = 0) {
val userId: Int = LiveStore.user.value?.uid ?: return val userId: Int = LiveStore.user.value?.uid ?: return
userSessionRepository.deleteUserSession(UserSessionCrossRef(userId, session.uid, count)) runInScope(
refreshState() actionSuccess = {
userSessionRepository.deleteUserSession(
UserSessionCrossRef(
userId,
session.uid,
count
)
)
refreshState()
}
)
} }
suspend fun updateFromCart(session: Session, count: Int, availableCount: Int): Boolean { suspend fun updateFromCart(session: Session, count: Int, availableCount: Int) {
val userId: Int = LiveStore.user.value?.uid ?: return false val userId: Int = LiveStore.user.value?.uid ?: return
if (count == 0) { if (count == 0) {
removeFromCart(session, count) removeFromCart(session, count)
return false return
} }
if (count > availableCount) if (count > availableCount)
return false return
userSessionRepository.updateUserSession(UserSessionCrossRef(userId, session.uid, count)) runInScope(
refreshState() actionSuccess = {
return true userSessionRepository.updateUserSession(
UserSessionCrossRef(
userId,
session.uid,
count
)
)
refreshState()
},
actionError = { },
needLoadingScreen = false
)
} }
} }

View File

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

View File

@ -1,6 +1,5 @@
package com.example.myapplication.composeui package com.example.myapplication.composeui
import android.content.res.Configuration
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer 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.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -17,12 +14,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign 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.TextUnit
import androidx.compose.ui.unit.TextUnitType import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.example.myapplication.R import com.example.myapplication.R
import com.example.myapplication.ui.theme.PmudemoTheme
@Composable @Composable
@ -67,30 +62,4 @@ fun LoadingPlaceholder() {
text = stringResource(id = R.string.loading) 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.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel 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.api.session.ReportRemote
import com.example.myapplication.composeui.navigation.Screen
import com.example.myapplication.database.entities.composeui.AppViewModelProvider import com.example.myapplication.database.entities.composeui.AppViewModelProvider
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.threeten.bp.format.DateTimeFormatter import org.threeten.bp.format.DateTimeFormatter
@ -34,62 +37,73 @@ import java.util.Date
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun Report( fun Report(
navController: NavController,
viewModel: ReportViewModel = viewModel(factory = AppViewModelProvider.Factory) viewModel: ReportViewModel = viewModel(factory = AppViewModelProvider.Factory)
) { ) {
val dateStateStart = rememberDatePickerState(initialDisplayMode = DisplayMode.Input) val dateStateStart = rememberDatePickerState(initialDisplayMode = DisplayMode.Input)
val dateStateEnd = rememberDatePickerState(initialDisplayMode = DisplayMode.Input) val dateStateEnd = rememberDatePickerState(initialDisplayMode = DisplayMode.Input)
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
Column(
modifier = Modifier when (viewModel.apiStatus) {
.verticalScroll(rememberScrollState()) ApiStatus.DONE -> {
.fillMaxWidth() Column(
.padding(all = 10.dp), modifier = Modifier
verticalArrangement = Arrangement.Center, .verticalScroll(rememberScrollState())
horizontalAlignment = Alignment.CenterHorizontally .fillMaxWidth()
) .padding(all = 10.dp),
{ verticalArrangement = Arrangement.Center,
Text( horizontalAlignment = Alignment.CenterHorizontally
text = "Начало периода",
style = MaterialTheme.typography.headlineLarge
)
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 = "Начало периода",
Text( style = MaterialTheme.typography.headlineLarge
text = "Конец периода",
style = MaterialTheme.typography.headlineLarge
)
DatePicker(state = dateStateEnd)
val selectedDateEnd = dateStateEnd.selectedDateMillis
if (selectedDateEnd != null) {
viewModel.onUpdate(
viewModel.reportUiState.reportDetails.copy(
endDate =
Date(selectedDateEnd)
) )
) DatePicker(state = dateStateStart)
} else { val selectedDateStart = dateStateStart.selectedDateMillis
viewModel.onUpdate(viewModel.reportUiState.reportDetails.copy(endDate = Date(0))) 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() } }, ApiStatus.LOADING -> LoadingPlaceholder()
enabled = viewModel.reportUiState.isEntryValid, else -> ErrorPlaceholder(
shape = MaterialTheme.shapes.small, message = viewModel.apiError,
modifier = Modifier.fillMaxWidth() onBack = { navController.navigate(Screen.Report.route) }
) { )
Text(text = "Получить отчет")
}
Spacer(modifier = Modifier.height(16.dp))
CardScreen(reportData = viewModel.reportResultUiState.report)
} }
} }

View File

@ -3,12 +3,11 @@ package com.example.myapplication.composeui
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import com.example.myapplication.api.session.ReportRemote import com.example.myapplication.api.session.ReportRemote
import com.example.myapplication.api.session.RestSessionRepository import com.example.myapplication.api.session.RestSessionRepository
import java.util.Date import java.util.Date
class ReportViewModel(private val serialRepository: RestSessionRepository) : ViewModel() { class ReportViewModel(private val serialRepository: RestSessionRepository) : MyViewModel() {
var reportUiState by mutableStateOf(ReportUiState()) var reportUiState by mutableStateOf(ReportUiState())
private set private set
@ -32,11 +31,17 @@ class ReportViewModel(private val serialRepository: RestSessionRepository) : Vie
suspend fun getReport() { suspend fun getReport() {
if (validateInput()) { if (validateInput()) {
val temp = serialRepository.getReport( runInScope(
reportUiState.reportDetails.startDate, actionSuccess = {
reportUiState.reportDetails.endDate 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)) Spacer(modifier = Modifier.width(16.dp))
if (currentScreen?.route == Screen.CinemaList.route) { Search(
Search( initValue = LiveStore.searchRequest.value ?: "",
initValue = LiveStore.searchRequest.value ?: "", onDone = {
onDone = { navController.navigate(Screen.CinemaList.route)
LiveStore.searchRequest.value = it LiveStore.searchRequest.value = it
}, },
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.height(36.dp) .height(36.dp)
.background( .background(
color = MaterialTheme.colorScheme.onPrimary, color = MaterialTheme.colorScheme.onPrimary,
RoundedCornerShape(18.dp) RoundedCornerShape(18.dp)
) )
.padding(start = 13.dp, top = 8.dp) .padding(start = 13.dp, top = 8.dp)
) )
}
} }
} }
} }
@ -163,8 +162,8 @@ fun Navhost(
) { ) {
composable(Screen.CinemaList.route) { CinemaList(navController) } composable(Screen.CinemaList.route) { CinemaList(navController) }
composable(Screen.OrderList.route) { OrderList(navController) } composable(Screen.OrderList.route) { OrderList(navController) }
composable(Screen.Cart.route) { Cart() } composable(Screen.Cart.route) { Cart(navController) }
composable(Screen.UserProfile.route) { UserProfile(isDarkTheme, dataStore, navController) } composable(Screen.UserProfile.route) { UserProfile(isDarkTheme, dataStore) }
composable( composable(
Screen.CinemaEdit.route, Screen.CinemaEdit.route,
arguments = listOf(navArgument("id") { type = NavType.IntType }) arguments = listOf(navArgument("id") { type = NavType.IntType })
@ -188,9 +187,9 @@ fun Navhost(
Screen.OrderView.route, Screen.OrderView.route,
arguments = listOf(navArgument("id") { type = NavType.IntType }) arguments = listOf(navArgument("id") { type = NavType.IntType })
) { backStackEntry -> ) { 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) { LaunchedEffect(searchPattern.value) {
viewModel.refresh() viewModel.refresh()
} }
Scaffold( Scaffold(
topBar = {}, topBar = {},
floatingActionButton = { floatingActionButton = {
@ -99,6 +98,7 @@ fun CinemaList(
} }
} }
@Composable @Composable
private fun CinemaList( private fun CinemaList(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,

View File

@ -21,12 +21,16 @@ class CinemaListViewModel(
fun refresh() { fun refresh() {
val name = "%${LiveStore.searchRequest.value}%" val name = "%${LiveStore.searchRequest.value}%"
val pagingSource = cinemaRepository.getAllCinemas(name) runInScope(actionSuccess = {
cinemaPagingFlow = CinemaPagingFlowState(pagingSource.cachedIn(viewModelScope)) val pagingSource = cinemaRepository.getAllCinemas(name)
cinemaPagingFlow = CinemaPagingFlowState(pagingSource.cachedIn(viewModelScope))
})
} }
suspend fun deleteCinema(cinema: Cinema) { 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.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import com.example.myapplication.LiveStore 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.composeui.navigation.Screen
import com.example.myapplication.database.entities.model.Cinema import com.example.myapplication.database.entities.model.Cinema
import com.example.myapplication.database.entities.model.UserRole import com.example.myapplication.database.entities.model.UserRole
@ -42,104 +45,112 @@ fun CinemaView(
) { ) {
val cinemaUiState = viewModel.cinemaUiState val cinemaUiState = viewModel.cinemaUiState
val user = LiveStore.user.observeAsState() val user = LiveStore.user.observeAsState()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.refreshState() viewModel.refreshState()
} }
when (viewModel.apiStatus) {
Column( ApiStatus.DONE -> {
modifier = Modifier Column(
.padding(16.dp)
.fillMaxSize(),
) {
val cinema: Cinema? = cinemaUiState.cinemaWithSessions?.cinema
if (cinema != null) {
Box(
modifier = Modifier modifier = Modifier
.background( .padding(16.dp)
color = MaterialTheme.colorScheme.secondary, .fillMaxSize(),
shape = RoundedCornerShape(16.dp)
)
) { ) {
Column( val cinema: Cinema? = cinemaUiState.cinemaWithSessions?.cinema
modifier = Modifier if (cinema != null) {
.fillMaxWidth() Box(
.padding(16.dp) modifier = Modifier
.background(color = MaterialTheme.colorScheme.secondary), .background(
verticalArrangement = Arrangement.spacedBy(8.dp) color = MaterialTheme.colorScheme.secondary,
) { shape = RoundedCornerShape(16.dp)
Row( )
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
Text( Column(
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,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(200.dp) .padding(16.dp)
.padding(4.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( if (cinema.image != null)
text = cinema.description, Image(
color = MaterialTheme.colorScheme.onSecondary bitmap = BitmapFactory.decodeByteArray(
) cinema.image,
} 0,
} cinema.image.size
} ).asImageBitmap(),
contentDescription = null,
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.padding(4.dp)
)
Row( Text(
verticalAlignment = Alignment.CenterVertically text = cinema.description,
) { color = MaterialTheme.colorScheme.onSecondary
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) }
} }
}
Row(
verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Text(
imageVector = Icons.Filled.Add, text = "Сеансы",
contentDescription = "Добавить сеанс", 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.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.SavedStateHandle 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.model.CinemaWithSessions
import com.example.myapplication.database.entities.repository.CinemaRepository import com.example.myapplication.database.entities.repository.CinemaRepository
class CinemaViewModel( class CinemaViewModel(
savedStateHandle: SavedStateHandle, private val cinemaRepository: CinemaRepository savedStateHandle: SavedStateHandle, private val cinemaRepository: CinemaRepository
) : ViewModel() { ) : MyViewModel() {
private val cinemaUid: Int = checkNotNull(savedStateHandle["id"]) private val cinemaUid: Int = checkNotNull(savedStateHandle["id"])
var cinemaUiState by mutableStateOf(CinemaUiState()) var cinemaUiState by mutableStateOf(CinemaUiState())
@ -18,7 +18,11 @@ class CinemaViewModel(
suspend fun refreshState() { suspend fun refreshState() {
if (cinemaUid > 0) { 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.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@ -26,7 +24,6 @@ import androidx.navigation.NavController
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemContentType import androidx.paging.compose.itemContentType
import androidx.paging.compose.itemKey import androidx.paging.compose.itemKey
import com.example.myapplication.LiveStore
import com.example.myapplication.composeui.navigation.Screen import com.example.myapplication.composeui.navigation.Screen
import com.example.myapplication.ui.theme.PmudemoTheme import com.example.myapplication.ui.theme.PmudemoTheme
import org.threeten.bp.format.DateTimeFormatter import org.threeten.bp.format.DateTimeFormatter
@ -36,11 +33,8 @@ fun OrderList(
navController: NavController?, navController: NavController?,
viewModel: OrderListViewModel = viewModel(factory = AppViewModelProvider.Factory) viewModel: OrderListViewModel = viewModel(factory = AppViewModelProvider.Factory)
) { ) {
val user = LiveStore.user.observeAsState()
val ordersUiState = viewModel.orderListUiState.collectAsLazyPagingItems() val ordersUiState = viewModel.orderListUiState.collectAsLazyPagingItems()
LaunchedEffect(user.value?.uid) {
viewModel.refreshState(user.value?.uid ?: 0)
}
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,17 +1,20 @@
package com.example.myapplication.database.entities.composeui package com.example.myapplication.database.entities.composeui
import android.util.Log
import android.widget.Toast
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import com.example.myapplication.MainComposeActivity
import com.example.myapplication.R 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.model.User
import com.example.myapplication.database.entities.repository.UserRepository import com.example.myapplication.database.entities.repository.UserRepository
import com.example.myapplication.datastore.DataStoreManager import com.example.myapplication.datastore.DataStoreManager
class UserProfileViewModel( class UserProfileViewModel(
private val userRepository: UserRepository private val userRepository: UserRepository
) : ViewModel() { ) : MyViewModel() {
var userUiState by mutableStateOf(UserUiState()) var userUiState by mutableStateOf(UserUiState())
private set 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 userUiState.details.passwordConfirm = userUiState.details.password
var errorId: Int? = validateInput(userUiState.details) var errorId: Int? = validateInput(userUiState.details)
if (errorId == null) { runInScope(
val overlap = userRepository.getUser(userUiState.details.login) actionSuccess = {
if (overlap == null || userUiState.details.password != overlap.password) { if (errorId == null) {
errorId = R.string.err_04 val overlap: User? = userRepository.getUser(userUiState.details.login)
} if (overlap == null || userUiState.details.password != overlap.password) {
} errorId = R.string.err_04
userUiState = UserUiState( } else {
details = userUiState.details, dataStoreManager.setLogin(userUiState.details.login)
errorId = errorId Log.d("UserProfileViewModel", "sign in success")
) }
if (errorId == null) { }
dataStoreManager.setLogin(userUiState.details.login) userUiState = UserUiState(
return true details = userUiState.details,
} errorId = errorId
return false )
}, 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) var errorId: Int? = validateInput(userUiState.details)
if (errorId == null) { runInScope(actionSuccess = {
val overlap = userRepository.getUser(userUiState.details.login) if (errorId == null) {
if (overlap != null) { val overlap = userRepository.getUser(userUiState.details.login)
errorId = R.string.err_03 if (overlap != null) {
errorId = R.string.err_03
}
} }
} if (errorId == null) {
userUiState = UserUiState( userRepository.insertUser(userUiState.details.toUser())
details = userUiState.details, val toast = Toast.makeText(
errorId = errorId MainComposeActivity.appContext,
) "Вы зарегистрированы",
if (errorId == null) { Toast.LENGTH_SHORT
userRepository.insertUser(userUiState.details.toUser()) )
return true toast.show()
} errorId = 0
return false }
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? { 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.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import com.example.myapplication.R 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.database.entities.composeui.AppViewModelProvider
import com.example.myapplication.ui.theme.PmudemoTheme import com.example.myapplication.ui.theme.PmudemoTheme
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -32,17 +35,25 @@ fun CinemaEdit(
viewModel: CinemaEditViewModel = viewModel(factory = AppViewModelProvider.Factory), viewModel: CinemaEditViewModel = viewModel(factory = AppViewModelProvider.Factory),
) { ) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
when (viewModel.apiStatus) {
CinemaEdit( ApiStatus.DONE -> {
cinemaUiState = viewModel.cinemaUiState, CinemaEdit(
onClick = { cinemaUiState = viewModel.cinemaUiState,
coroutineScope.launch { onClick = {
viewModel.saveCinema() coroutineScope.launch {
navController.popBackStack() viewModel.saveCinema()
} navController.popBackStack()
}, }
onUpdate = viewModel::updateUiState, },
) onUpdate = viewModel::updateUiState,
)
}
ApiStatus.LOADING -> LoadingPlaceholder()
else -> ErrorPlaceholder(
message = viewModel.apiError,
onBack = { navController.popBackStack() }
)
}
} }
@Composable @Composable
@ -100,7 +111,6 @@ private fun CinemaEdit(
Button( Button(
onClick = onClick, onClick = onClick,
enabled = cinemaUiState.isEntryValid, enabled = cinemaUiState.isEntryValid,
shape = MaterialTheme.shapes.small,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
Text(text = stringResource(R.string.Save_button)) 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.DatePicker
import androidx.compose.material3.DisplayMode import androidx.compose.material3.DisplayMode
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TimePicker import androidx.compose.material3.TimePicker
@ -24,6 +23,9 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import com.example.myapplication.R 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.database.entities.composeui.AppViewModelProvider
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.threeten.bp.Instant import org.threeten.bp.Instant
@ -39,16 +41,26 @@ fun SessionEdit(
viewModel: SessionEditViewModel = viewModel(factory = AppViewModelProvider.Factory), viewModel: SessionEditViewModel = viewModel(factory = AppViewModelProvider.Factory),
) { ) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
SessionEdit( when (viewModel.apiStatus) {
sessionUiState = viewModel.sessionUiState, ApiStatus.DONE -> {
onClick = { SessionEdit(
coroutineScope.launch { sessionUiState = viewModel.sessionUiState,
viewModel.saveSession() onClick = {
navController.popBackStack() coroutineScope.launch {
} viewModel.saveSession()
}, navController.popBackStack()
onUpdate = viewModel::updateUiState }
) },
onUpdate = viewModel::updateUiState
)
}
ApiStatus.LOADING -> LoadingPlaceholder()
else -> ErrorPlaceholder(
message = viewModel.apiError,
onBack = { navController.popBackStack() }
)
}
} }
fun Long.toLocalDate(): org.threeten.bp.LocalDate { fun Long.toLocalDate(): org.threeten.bp.LocalDate {
@ -140,7 +152,6 @@ private fun SessionEdit(
Button( Button(
onClick = onClick, onClick = onClick,
enabled = sessionUiState.isEntryValid, enabled = sessionUiState.isEntryValid,
shape = MaterialTheme.shapes.small,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
Text(text = stringResource(R.string.Save_button)) Text(text = stringResource(R.string.Save_button))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import androidx.paging.Pager
import androidx.paging.PagingConfig import androidx.paging.PagingConfig
import androidx.paging.PagingData import androidx.paging.PagingData
import androidx.paging.PagingSource import androidx.paging.PagingSource
import com.example.myapplication.LiveStore
import com.example.myapplication.database.AppContainer import com.example.myapplication.database.AppContainer
import com.example.myapplication.database.entities.dao.OrderDao import com.example.myapplication.database.entities.dao.OrderDao
import com.example.myapplication.database.entities.model.Order 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 import kotlinx.coroutines.flow.Flow
class OfflineOrderRepository(private val orderDao: OrderDao) : OrderRepository { 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( config = PagingConfig(
pageSize = AppContainer.LIMIT, pageSize = AppContainer.LIMIT,
enablePlaceholders = false enablePlaceholders = false
), ),
pagingSourceFactory = { orderDao.getAll(userId) } pagingSourceFactory = { orderDao.getAll(LiveStore.user.value?.uid ?: 0) }
).flow ).flow
override suspend fun getOrder(uid: Int): List<SessionFromOrder> = orderDao.getByUid(uid) override suspend fun getOrder(uid: Int): List<SessionFromOrder> = orderDao.getByUid(uid)
override suspend fun insertOrder(order: Order): Long = orderDao.insert(order).first() 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() 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(userId: Int) = userSessionDao.deleteByUserUid(userId)
override suspend fun deleteUserSessions(userSessionCrossRefs: List<UserSessionCrossRef>) =
userSessionDao.delete(*userSessionCrossRefs.toTypedArray())
suspend fun deleteSessionsByUid(sessionId: Int) = userSessionDao.deleteBySessionUid(sessionId) 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 import kotlinx.coroutines.flow.Flow
interface OrderRepository { interface OrderRepository {
fun getAllOrders(userId: Int): Flow<PagingData<Order>> fun getAllOrders(): Flow<PagingData<Order>>
suspend fun getOrder(uid: Int): List<SessionFromOrder> suspend fun getOrder(uid: Int): List<SessionFromOrder>
suspend fun insertOrder(order: Order): Long suspend fun insertOrder(order: Order): Long
} }

View File

@ -7,4 +7,5 @@ interface UserSessionRepository {
suspend fun updateUserSession(userSessionCrossRef: UserSessionCrossRef) suspend fun updateUserSession(userSessionCrossRef: UserSessionCrossRef)
suspend fun deleteUserSession(userSessionCrossRef: UserSessionCrossRef) suspend fun deleteUserSession(userSessionCrossRef: UserSessionCrossRef)
suspend fun deleteUserSessions(userId: Int) 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_03">Логин занят</string>
<string name="err_04">Неверный логин или пароль</string> <string name="err_04">Неверный логин или пароль</string>
<string name="err_05">Не совпадают пароли</string> <string name="err_05">Не совпадают пароли</string>
<string name="err_06">Ошибка сети</string>
<string name="back">Назад</string> <string name="back">Назад</string>
<string name="loading">Загрузка…</string> <string name="loading">Загрузка…</string>
</resources> </resources>

File diff suppressed because it is too large Load Diff

View File

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