отчет

This commit is contained in:
dasha 2023-12-15 19:49:41 +04:00
parent d45b6d7ed8
commit aab18402b5
31 changed files with 171655 additions and 39363 deletions

View File

@ -2,10 +2,14 @@ package com.example.myapplication.api
import com.example.myapplication.api.cinema.CinemaRemote
import com.example.myapplication.api.order.OrderRemote
import com.example.myapplication.api.cinema.CinemaWithSessionsRemote
import com.example.myapplication.api.session.ReportRemote
import com.example.myapplication.api.session.SessionFromCinemaRemote
import com.example.myapplication.api.session.SessionRemote
import com.example.myapplication.api.session.SessionWithCinemaRemote
import com.example.myapplication.api.user.UserRemote
import com.example.myapplication.api.user.UserSessionRemote
import com.example.myapplication.api.user.UserSessionWithSessionRemote
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
@ -21,9 +25,10 @@ import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path
import retrofit2.http.Query
import java.util.Date
interface MyServerService {
/*@GET("orders")
@GET("orders")
suspend fun getOrders(): List<OrderRemote>
@GET("users")
@ -40,6 +45,11 @@ interface MyServerService {
@Path("id") id: Int,
): CinemaRemote
@GET("cinemas/{id}?_embed=sessions")
suspend fun getCinemaWithSessions(
@Path("id") id: Int,
): CinemaWithSessionsRemote
@POST("cinemas")
suspend fun createCinema(
@Body cinema: CinemaRemote,
@ -56,11 +66,6 @@ interface MyServerService {
@Path("id") id: Int,
)
@GET("cinemas/{cinemaId}/sessions")
suspend fun getSessionsForCinema(
@Path("cinemaId") cinemaId: Int
): List<SessionFromCinemaRemote>
@GET("sessions/{id}?_expand=cinema")
suspend fun getSession(
@Path("id") id: Int,
@ -82,21 +87,40 @@ interface MyServerService {
@Path("id") id: Int,
): SessionFromCinemaRemote
@GET("users/{id}")
@GET("userssessions?_expand=session")
suspend fun getUserCart(
@Query("userId") userId: Int,
): List<UserSessionWithSessionRemote>
@GET("userssessions?_expand=session")
suspend fun getUsersSessions(): List<UserSessionWithSessionRemote>
@DELETE("userssessions/{id}")
suspend fun deleteUserSession(
@Path("id") id: Int,
): UserRemote
): UserSessionRemote
@GET("users?_limit=1")
suspend fun getUser(
@Query("login") login: String,
): List<UserRemote>
@PUT("users/{id}")
@GET("userssessions?_limit=1")
suspend fun getUserSession(
@Query("userId") userId: Int,
@Query("sessionId") sessionId: Int,
): List<UserSessionRemote>
@POST("userssessions")
suspend fun createUserSession(
@Body userSessionRemote: UserSessionRemote,
): UserSessionRemote
@PUT("userssessions/{id}")
suspend fun updateUserCart(
@Path("id") id: Int,
@Body userRemote: UserRemote,
): UserRemote
@Body userSessionRemote: UserSessionRemote,
): UserSessionRemote
@POST("users")
suspend fun createUser(
@ -105,6 +129,7 @@ interface MyServerService {
@GET("orders")
suspend fun getOrders(
@Query("userId") userId: Int,
@Query("_page") page: Int,
@Query("_limit") limit: Int,
): List<OrderRemote>
@ -123,11 +148,17 @@ interface MyServerService {
suspend fun updateOrder(
@Path("id") id: Int,
@Body orderRemote: OrderRemote,
): OrderRemote*/
): OrderRemote
@GET("report")
suspend fun getReport(
@Query("startDate") startDate: Date,
@Query("endDate") endDate: Date
): List<ReportRemote>
companion object {
//private const val BASE_URL = "http://192.168.154.166:8080/"
private const val BASE_URL = "http://192.168.0.101:8080/"
private const val BASE_URL = "http://192.168.0.101:8079/"
@Volatile
private var INSTANCE: MyServerService? = null

View File

@ -1,12 +1,12 @@
package com.example.myapplication.api.cinema
import android.database.sqlite.SQLiteConstraintException
import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadType
import androidx.paging.PagingState
import androidx.paging.RemoteMediator
import androidx.room.withTransaction
import com.example.myapplication.api.MyServerService
import com.example.myapplication.api.session.toSession
import com.example.myapplication.database.AppDatabase
import com.example.myapplication.database.entities.model.Cinema
import com.example.myapplication.database.entities.repository.OfflineCinemaRepository
@ -55,10 +55,8 @@ class CinemaRemoteMediator(
try {
val cinemas = service.getCinemas(page, state.config.pageSize).map { it.toCinema() }
val cinemasWithSessions = cinemas.map { cinema ->
service.getSessionsForCinema(cinema.uid).map {
service.getSession(it.id).toSession()
}
val sessionsFromCinemas = cinemas.map { cinema ->
service.getCinemaWithSessions(cinema.uid).toSessions()
}
val endOfPaginationReached = cinemas.isEmpty()
database.withTransaction {
@ -79,10 +77,11 @@ class CinemaRemoteMediator(
}
dbRemoteKeyRepository.createRemoteKeys(keys)
dbCinemaRepository.insertCinemas(cinemas)
cinemasWithSessions.forEach {
sessionsFromCinemas.forEach {
try {
dbSessionRepository.insertSessions(it)
} catch (_: Exception) {
dbSessionRepository.insertSessions(it)
} catch(_:Exception) {
}
}
}
@ -91,6 +90,8 @@ class CinemaRemoteMediator(
return MediatorResult.Error(exception)
} catch (exception: HttpException) {
return MediatorResult.Error(exception)
} catch (exception: SQLiteConstraintException) {
return MediatorResult.Error(exception)
}
}

View File

@ -0,0 +1,31 @@
package com.example.myapplication.api.cinema
import com.example.myapplication.api.session.SessionFromCinemaRemote
import com.example.myapplication.api.session.toSessionFromCinema
import com.example.myapplication.database.entities.model.Cinema
import com.example.myapplication.database.entities.model.Session
import com.example.myapplication.database.entities.model.toSession
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class CinemaWithSessionsRemote(
val id: Int = 0,
val name: String = "",
val description: String = "",
val image: ByteArray? = null,
val year: Long = 0,
@SerialName("sessions")
val sessions: List<SessionFromCinemaRemote>,
)
fun CinemaWithSessionsRemote.toCinema(): Cinema = Cinema(
id,
name,
description,
image,
year
)
fun CinemaWithSessionsRemote.toSessions(): List<Session> =
sessions.map { it.toSessionFromCinema().toSession() }

View File

@ -47,20 +47,25 @@ class RestCinemaRepository(
}
override suspend fun getCinema(uid: Int): CinemaWithSessions {
val cinema = service.getCinema(uid).toCinema()
val cinemaWithSessions = service.getCinemaWithSessions(uid)
val sessions = service.getSessionsForCinema(uid).map { x ->
val sessions = cinemaWithSessions.sessions.map { sessionFromCinemaRemote ->
SessionFromCinema(
x.id,
x.dateTime,
x.price,
x.maxCount - service.getOrders().flatMap { order ->
order.sessions.filter { session -> session.id == x.id }
sessionFromCinemaRemote.id,
sessionFromCinemaRemote.dateTime,
sessionFromCinemaRemote.price,
sessionFromCinemaRemote.maxCount - service.getOrders().flatMap
{ order ->
order.sessions.filter { session ->
session.id == sessionFromCinemaRemote.id &&
session.cinemaId == sessionFromCinemaRemote.cinemaId &&
session.cinema.name == cinemaWithSessions.name
}
}.sumOf { session -> session.count },
uid
)
}
return CinemaWithSessions(cinema, sessions)
return CinemaWithSessions(cinemaWithSessions.toCinema(), sessions)
}
override suspend fun insertCinema(cinema: Cinema) {
@ -72,15 +77,11 @@ class RestCinemaRepository(
}
override suspend fun deleteCinema(cinema: Cinema) {
val cart = service.getUsers()
cart.forEach { userRemote ->
userRemote.sessions = userRemote.sessions.filter { x -> x.cinemaId != cinema.uid }
service.updateUserCart(userRemote.id, userRemote)
}
val orders = service.getOrders()
orders.forEach { orderRemote ->
orderRemote.sessions = orderRemote.sessions.filter { x -> x.cinemaId != cinema.uid }
service.updateOrder(orderRemote.id, orderRemote)
val cart = service.getUsersSessions()
cart.forEach { userSessionRemote ->
if (userSessionRemote.session.cinemaId == cinema.uid) {
service.deleteUserSession(userSessionRemote.id)
}
}
service.deleteCinema(cinema.uid)
dbCinemaRepository.deleteCinema(cinema)

View File

@ -5,6 +5,7 @@ import androidx.paging.LoadType
import androidx.paging.PagingState
import androidx.paging.RemoteMediator
import androidx.room.withTransaction
import com.example.myapplication.LiveStore
import com.example.myapplication.api.MyServerService
import com.example.myapplication.database.AppDatabase
import com.example.myapplication.database.entities.model.Order
@ -51,7 +52,8 @@ class OrderRemoteMediator(
}
try {
val orders = service.getOrders(page, state.config.pageSize).map { it.toOrder() }
val orders = service.getOrders(LiveStore.user.value?.uid ?: 0,
page, state.config.pageSize).map { it.toOrder() }
val endOfPaginationReached = orders.isEmpty()
database.withTransaction {
if (loadType == LoadType.REFRESH) {

View File

@ -63,7 +63,7 @@ class RestOrderRepository(
)
)
}
return order.sessions.map { x -> x.toSessionFromOrder(dbCinemaRepository.getCinema(x.cinemaId).cinema.toCinemaRemote()) }
return order.sessions.map { x -> x.toSessionFromOrder() }
}
override suspend fun insertOrder(order: Order): Long {

View File

@ -13,14 +13,15 @@ class RestOrderSessionRepository(
) : OrderSessionRepository {
override suspend fun insertOrderSession(orderSessionCrossRef: OrderSessionCrossRef) {
var orderRemote = service.getOrder(orderSessionCrossRef.orderId)
val session = service.getSession(orderSessionCrossRef.sessionId).toSession()
val session = service.getSession(orderSessionCrossRef.sessionId)
val sessionFromOrder = SessionFromOrderRemote(
session.uid,
session.id,
session.dateTime,
session.price,
orderSessionCrossRef.count,
session.cinemaId
session.cinemaId,
session.cinema
)
val updatedSessions = orderRemote.sessions.toMutableList()

View File

@ -1,71 +0,0 @@
package com.example.myapplication.api.session
import com.example.myapplication.api.cinema.CinemaRemote
import com.example.myapplication.api.cinema.toCinema
import com.example.myapplication.api.session.SessionFromCinemaRemote
import com.example.myapplication.api.session.toSessionFromCinema
import com.example.myapplication.database.entities.model.Cinema
import com.example.myapplication.database.entities.model.CinemaWithSessions
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/*
@Serializable
data class CinemaWithSessionsRemote(
val id: Int = 0,
val name: String = "",
val description: String = "",
val image: ByteArray? = null,
val year: Long = 0,
@SerialName("sessions")
val sessions: List<SessionFromCinemaRemote>,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CinemaWithSessionsRemote
if (id != other.id) return false
if (name != other.name) return false
if (description != other.description) return false
if (image != null) {
if (other.image == null) return false
if (!image.contentEquals(other.image)) return false
} else if (other.image != null) return false
if (year != other.year) return false
if (sessions != other.sessions) return false
return true
}
override fun hashCode(): Int {
var result = id
result = 31 * result + name.hashCode()
result = 31 * result + description.hashCode()
result = 31 * result + (image?.contentHashCode() ?: 0)
result = 31 * result + year.hashCode()
result = 31 * result + sessions.hashCode()
return result
}
}
fun CinemaWithSessionsRemote.toCinemaWithSessions(): CinemaWithSessions = CinemaWithSessions(
Cinema(
id,
name,
description,
image,
year
),
sessions.map { x -> x.toSessionFromCinema() }
)
fun Cinema.toCinemaWithSessionsRemote(): CinemaWithSessionsRemote = CinemaWithSessionsRemote(
uid,
name,
description,
image,
year,
sessions = emptyList()
)*/

View File

@ -0,0 +1,21 @@
package com.example.myapplication.api.session
import kotlinx.serialization.Contextual
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ReportRemote(
@SerialName("cinema_name")
val cinemaName: String = "",
@Contextual
@SerialName("current_ticket_date_time")
val ticketDateTime: org.threeten.bp.LocalDateTime,
@SerialName("current_ticket_price")
val ticketPrice: Double = 0.0,
@SerialName("max_ticket_quantity")
val ticketQuantity: Int = 0,
@SerialName("purchased_tickets")
val ticketsPurchased: Int = 0,
val revenue: Double = 0.0
)

View File

@ -6,6 +6,7 @@ import com.example.myapplication.database.entities.repository.OfflineOrderSessio
import com.example.myapplication.database.entities.repository.OfflineSessionRepository
import com.example.myapplication.database.entities.repository.OfflineUserSessionRepository
import com.example.myapplication.database.entities.repository.SessionRepository
import java.util.Date
class RestSessionRepository(
private val service: MyServerService,
@ -33,10 +34,11 @@ class RestSessionRepository(
}
override suspend fun deleteSession(session: Session) {
val cart = service.getUsers()
cart.forEach { userRemote ->
userRemote.sessions = userRemote.sessions.filter { x -> x.id != session.uid }
service.updateUserCart(userRemote.id, userRemote)
val cart = service.getUsersSessions()
cart.forEach { userSessionRemote ->
if (userSessionRemote.session.id == session.uid) {
service.deleteUserSession(userSessionRemote.id)
}
}
val orders = service.getOrders()
orders.forEach { orderRemote ->
@ -48,4 +50,8 @@ class RestSessionRepository(
dbOrderSessionRepository.deleteSessionsByUid(session.uid)
dbSessionRepository.deleteSession(session)
}
suspend fun getReport(startDate: Date, endDate: Date): List<ReportRemote> {
return service.getReport(startDate, endDate)
}
}

View File

@ -1,6 +1,5 @@
package com.example.myapplication.api.session
import com.example.myapplication.database.entities.model.Session
import com.example.myapplication.database.entities.model.SessionFromCinema
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
@ -23,12 +22,4 @@ fun SessionFromCinemaRemote.toSessionFromCinema(): SessionFromCinema = SessionFr
price,
availableCount,
cinemaId
)
fun SessionFromCinema.toSessionFromCinemaRemote(): SessionFromCinemaRemote = SessionFromCinemaRemote(
uid,
dateTime,
price,
availableCount,
cinemaId
)

View File

@ -14,9 +14,10 @@ class SessionFromOrderRemote(
val frozenPrice: Double = 0.0,
val count: Int = 0,
val cinemaId: Int = 0,
val cinema: CinemaRemote,
)
fun SessionFromOrderRemote.toSessionFromOrder(cinema: CinemaRemote): SessionFromOrder =
fun SessionFromOrderRemote.toSessionFromOrder(): SessionFromOrder =
SessionFromOrder(
id, dateTime, frozenPrice, count, cinemaId, cinema.toCinema()
)

View File

@ -30,25 +30,21 @@ class RestUserRepository(
override suspend fun getCartByUser(userId: Int): List<SessionFromCart> {
val cart = service.getUserCart(userId)
dbUserSessionRepository.deleteUserSessions(userId)
cart.sessions.map { sessionFromCartRemote ->
cart.map { sessionFromCartRemote ->
dbUserSessionRepository.insertUserSession(
UserSessionCrossRef(
userId,
sessionFromCartRemote.id,
sessionFromCartRemote.sessionId,
sessionFromCartRemote.count
)
)
}
return cart.sessions.map {
val session = service.getSession(it.id)
return cart.map {
val cinema = service.getCinema(it.session.cinemaId)
it.toSessionFromCart(
session.cinema,
session.dateTime,
session.price,
session.maxCount - service.getOrders().flatMap { order ->
it.session.maxCount - service.getOrders().flatMap { order ->
order.sessions.filter { session -> session.id == it.id }
}.sumOf { session -> session.count })
}.sumOf { session -> session.count }, cinema.toCinema())
}
}

View File

@ -13,7 +13,6 @@ data class UserRemote(
val login: String = "",
val password: String = "",
val role: Int = -1,
var sessions: List<SessionFromCartRemote> = emptyList()
)
fun User.toUserRemote(): UserRemote = UserRemote(

View File

@ -0,0 +1,15 @@
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 com.example.myapplication.database.entities.model.User
import kotlinx.serialization.Serializable
@Serializable
data class UserSessionRemote (
val id: Int = 0,
val userId: Int = 0,
val sessionId: Int = 0,
var count: Int = 0,
)

View File

@ -0,0 +1,25 @@
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
data class UserSessionWithSessionRemote (
val id: Int = 0,
val userId: Int = 0,
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

@ -3,6 +3,7 @@ package com.example.myapplication.api.usersession
import com.example.myapplication.api.MyServerService
import com.example.myapplication.api.session.SessionFromCartRemote
import com.example.myapplication.api.session.toSession
import com.example.myapplication.api.user.UserSessionRemote
import com.example.myapplication.database.entities.model.UserSessionCrossRef
import com.example.myapplication.database.entities.repository.OfflineUserSessionRepository
import com.example.myapplication.database.entities.repository.UserSessionRepository
@ -12,51 +13,44 @@ class RestUserSessionRepository(
private val dbUserSessionRepository: OfflineUserSessionRepository
) : UserSessionRepository {
override suspend fun insertUserSession(userSessionCrossRef: UserSessionCrossRef) {
var cartSessions = service.getUserCart(userSessionCrossRef.userId)
cartSessions.sessions.forEach { session ->
if (session.id == userSessionCrossRef.sessionId)
val cartSessions = service.getUserCart(userSessionCrossRef.userId)
cartSessions.forEach { session ->
if (session.sessionId == userSessionCrossRef.sessionId)
return
}
val session = service.getSession(userSessionCrossRef.sessionId).toSession()
val sessionFromCart = SessionFromCartRemote(
session.uid,
userSessionCrossRef.count,
session.cinemaId,
)
val updatedSessions = cartSessions.sessions.toMutableList()
updatedSessions.add(sessionFromCart)
cartSessions = cartSessions.copy(sessions = updatedSessions)
service.updateUserCart(userSessionCrossRef.userId, cartSessions)
service.createUserSession(UserSessionRemote(id = 0,
userId = userSessionCrossRef.userId,
sessionId = userSessionCrossRef.sessionId,
count = userSessionCrossRef.count
))
dbUserSessionRepository.insertUserSession(userSessionCrossRef)
}
override suspend fun updateUserSession(userSessionCrossRef: UserSessionCrossRef) {
val userRemote = service.getUserCart(userSessionCrossRef.userId)
val userSessionRemote = service.getUserSession(userSessionCrossRef.userId,
userSessionCrossRef.sessionId).first()
if (userSessionCrossRef.count <= 0) {
userRemote.sessions =
userRemote.sessions.filter { x -> x.id != userSessionCrossRef.sessionId }
} else
userRemote.sessions.forEach {
if (it.id == userSessionCrossRef.sessionId) {
it.count = userSessionCrossRef.count
}
}
service.updateUserCart(userSessionCrossRef.userId, userRemote)
service.deleteUserSession(userSessionRemote.id)
dbUserSessionRepository.deleteUserSession(userSessionCrossRef)
return
}
userSessionRemote.count = userSessionCrossRef.count
service.updateUserCart(userSessionRemote.id, userSessionRemote)
dbUserSessionRepository.updateUserSession(userSessionCrossRef)
}
override suspend fun deleteUserSession(userSessionCrossRef: UserSessionCrossRef) {
updateUserSession(userSessionCrossRef)
val userSessionRemote = service.getUserSession(userSessionCrossRef.userId,
userSessionCrossRef.sessionId).first()
service.deleteUserSession(userSessionRemote.id)
dbUserSessionRepository.deleteUserSession(userSessionCrossRef)
}
override suspend fun deleteUserSessions(userId: Int) {
val userRemote = service.getUserCart(userId)
userRemote.sessions = emptyList()
service.updateUserCart(userId, userRemote)
val cart = service.getUserCart(userId)
cart.forEach {
service.deleteUserSession(it.id)
}
dbUserSessionRepository.deleteUserSessions(userId)
}
}

View File

@ -15,7 +15,6 @@ fun Authenticator(
dataStoreManager: DataStoreManager,
viewModel: AuthenticatorViewModel = viewModel(factory = AppViewModelProvider.Factory)
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val login = dataStoreManager.getLogin().collectAsState(initial = "").value

View File

@ -164,7 +164,7 @@ private fun SessionListItem(
modifier: Modifier = Modifier,
onChangeCount: (SessionFromCart, Int) -> Unit,
) {
var currentCount by remember { mutableIntStateOf(session.count) }
//var currentCount by remember { mutableIntStateOf(session.count) }
val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")
val formattedDate = dateFormatter.format(session.dateTime)
@ -203,7 +203,7 @@ private fun SessionListItem(
Text(
text = "${session.cinema.name}, ${session.cinema.year}\n" +
"Цена: ${session.price}\n" +
"${currentCount}/${session.availableCount}",
"${session.count}/${session.availableCount}",
color = MaterialTheme.colorScheme.onSecondary
)
}
@ -219,7 +219,7 @@ private fun SessionListItem(
verticalAlignment = Alignment.CenterVertically
) {
IconButton(
onClick = { onChangeCount(session, --currentCount) }
onClick = { onChangeCount(session, session.count - 1) }
) {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.minus),
@ -230,7 +230,7 @@ private fun SessionListItem(
}
Text(
text = "$currentCount",
text = "${session.count}",
color = MaterialTheme.colorScheme.onBackground
)
@ -238,7 +238,7 @@ private fun SessionListItem(
onClick = {
onChangeCount(
session,
if (currentCount != session.availableCount) ++currentCount else currentCount
if (session.count != session.availableCount) session.count + 1 else session.count
)
}
) {

View File

@ -15,6 +15,7 @@ import com.example.myapplication.database.entities.repository.OrderSessionReposi
import com.example.myapplication.database.entities.repository.UserRepository
import com.example.myapplication.database.entities.repository.UserSessionRepository
import org.threeten.bp.LocalDateTime
import java.util.LinkedList
class CartViewModel(
private val userSessionRepository: UserSessionRepository,
@ -22,6 +23,19 @@ class CartViewModel(
private val orderSessionRepository: OrderSessionRepository,
private val userRepository: UserRepository,
) : ViewModel() {
private val requestQueue: LinkedList<suspend () -> Unit> = LinkedList()
private var isProcessingQueue: Boolean = false
private suspend fun processQueue() {
isProcessingQueue = true
while (requestQueue.isNotEmpty()) {
val request = requestQueue.poll()
request.invoke()
refreshState()
}
isProcessingQueue = false
}
var isLoading: Boolean = false
var cartUiState by mutableStateOf(CartUiState())
private set
@ -71,6 +85,7 @@ class CartViewModel(
isLoading = true
val userId: Int = LiveStore.user.value?.uid ?: return false
if (count == 0) {
isLoading = false
removeFromCart(session, count)
return false
}

View File

@ -0,0 +1,121 @@
package com.example.myapplication.composeui
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
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.Text
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.myapplication.api.session.ReportRemote
import com.example.myapplication.database.entities.composeui.AppViewModelProvider
import kotlinx.coroutines.launch
import org.threeten.bp.format.DateTimeFormatter
import java.util.Date
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Report(
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)
)
)
} 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,
shape = MaterialTheme.shapes.small,
modifier = Modifier.fillMaxWidth()
) {
Text(text = "Получить отчет")
}
Spacer(modifier = Modifier.height(16.dp))
CardScreen(reportData = viewModel.reportResultUiState.report)
}
}
@Composable
fun CardScreen(reportData: List<ReportRemote>) {
val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")
reportData.forEach {
val (cinemaName, ticketDateTime, ticketPrice, ticketQuantity, ticketsPurchased, revenue) = it
Row(
modifier = Modifier
.fillMaxWidth()
.border(width = 1.dp, color = Color.White, shape = MaterialTheme.shapes.small)
) {
Column(
Modifier
.padding(16.dp)
.background(color = Color.Transparent)
) {
Text(text = "Фильм: $cinemaName")
Text(text = "Сеанс: ${dateFormatter.format(ticketDateTime)}")
Text(text = "Стоимость: $ticketPrice")
Text(text = "Максимальное количество билетов: $ticketQuantity")
Text(text = "Купили: $ticketsPurchased")
Text(text = "Выручка: $revenue")
}
}
Spacer(modifier = Modifier.height(16.dp))
}
}

View File

@ -0,0 +1,56 @@
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() {
var reportUiState by mutableStateOf(ReportUiState())
private set
var reportResultUiState by mutableStateOf(ReportResultUiState())
private set
fun onUpdate(reportDetails: ReportDetails) {
reportUiState = ReportUiState(
reportDetails = reportDetails,
isEntryValid = validateInput(reportDetails)
)
}
private fun validateInput(uiState: ReportDetails = reportUiState.reportDetails): Boolean {
return with(uiState) {
startDate != Date(0)
&& endDate != Date(0)
&& startDate <= endDate
}
}
suspend fun getReport() {
if (validateInput()) {
val temp = serialRepository.getReport(
reportUiState.reportDetails.startDate,
reportUiState.reportDetails.endDate
)
reportResultUiState = ReportResultUiState(temp)
}
}
}
data class ReportDetails(
val startDate: Date = Date(0),
val endDate: Date = Date(0)
)
data class ReportUiState(
val reportDetails: ReportDetails = ReportDetails(),
val isEntryValid: Boolean = false
)
data class ReportResultUiState(
val report: List<ReportRemote> = emptyList()
)

View File

@ -19,7 +19,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
@ -29,6 +28,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@ -46,7 +46,9 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.example.myapplication.LiveStore
import com.example.myapplication.composeui.Cart
import com.example.myapplication.composeui.Report
import com.example.myapplication.database.entities.composeui.CinemaList
import com.example.myapplication.database.entities.composeui.CinemaView
import com.example.myapplication.database.entities.composeui.OrderList
@ -54,6 +56,7 @@ import com.example.myapplication.database.entities.composeui.OrderView
import com.example.myapplication.database.entities.composeui.UserProfile
import com.example.myapplication.database.entities.composeui.edit.CinemaEdit
import com.example.myapplication.database.entities.composeui.edit.SessionEdit
import com.example.myapplication.database.entities.model.UserRole
import com.example.myapplication.datastore.DataStoreManager
@Composable
@ -136,28 +139,31 @@ fun Navbar(
currentDestination: NavDestination?,
modifier: Modifier = Modifier
) {
val user = LiveStore.user.observeAsState()
NavigationBar(modifier = modifier, containerColor = MaterialTheme.colorScheme.primary) {
Screen.bottomBarItems.forEach { screen ->
NavigationBarItem(
icon = {
Icon(
screen.icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.secondary
)
},
label = { Text(stringResource(screen.resourceId)) },
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
onClick = {
navController.navigate(screen.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
if (screen.route != Screen.Report.route || user.value?.role == UserRole.ADMIN) {
NavigationBarItem(
icon = {
Icon(
screen.icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.secondary
)
},
label = { Text(stringResource(screen.resourceId)) },
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
onClick = {
navController.navigate(screen.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
launchSingleTop = true
restoreState = true
}
}
)
)
}
}
}
}
@ -204,10 +210,10 @@ fun Navhost(
) { backStackEntry ->
backStackEntry.arguments?.let { OrderView(it.getInt("id")) }
}
composable(Screen.Report.route) { Report() }
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainNavbar(
isDarkTheme: MutableState<Boolean>,

View File

@ -41,13 +41,17 @@ enum class Screen(
),
UserProfile(
"User-profile", R.string.Profile_title, showInBottomBar = false
),
Report(
"Report", R.string.Report_title,
);
companion object {
val bottomBarItems = listOf(
CinemaList,
Cart,
OrderList
OrderList,
Report
)
fun getItem(route: String): Screen? {

View File

@ -9,6 +9,7 @@ import com.example.myapplication.CinemaApplication
import com.example.myapplication.composeui.Authenticator
import com.example.myapplication.composeui.AuthenticatorViewModel
import com.example.myapplication.composeui.CartViewModel
import com.example.myapplication.composeui.ReportViewModel
import com.example.myapplication.database.entities.composeui.edit.CinemaEditViewModel
import com.example.myapplication.database.entities.composeui.edit.SessionEditViewModel
@ -68,6 +69,9 @@ object AppViewModelProvider {
initializer {
AuthenticatorViewModel(cinemaApplication().container.userRestRepository)
}
initializer {
ReportViewModel(cinemaApplication().container.sessionRestRepository)
}
}
}

View File

@ -1,26 +1,18 @@
package com.example.myapplication.database.entities.composeui
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData
import com.example.myapplication.LiveStore
import com.example.myapplication.database.AppDataContainer
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.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class OrderListViewModel(
private val orderRepository: OrderRepository
) : ViewModel() {
val orderListUiState: Flow<PagingData<Order>> = orderRepository.getAllOrders(LiveStore.user.value?.uid ?: 0)
val orderListUiState: Flow<PagingData<Order>> =
orderRepository.getAllOrders(LiveStore.user.value?.uid ?: 0)
}
data class OrderListUiState(val orderList: List<Order> = listOf())

View File

@ -46,7 +46,6 @@ fun UserProfile(
viewModel: UserProfileViewModel = viewModel(factory = AppViewModelProvider.Factory),
) {
var isRegistration by remember { mutableStateOf(false) }
val coroutineScope = rememberCoroutineScope()
val coroutine = rememberCoroutineScope()
val errorStringId: Int? = viewModel.userUiState.errorId
val errorMessage = if (errorStringId == null) "" else stringResource(errorStringId)
@ -54,78 +53,65 @@ fun UserProfile(
LazyColumn {
item {
Text(
text = "Текущий пользователь: " + (LiveStore.user.value?.login ?: ""),
)
Button(
enabled = user.value != null,
onClick = {
coroutineScope.launch {
dataStoreManager.setLogin("")
}
},
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
Text("Выход")
}
Text(
text = errorMessage,
color = Color.Red
)
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = "Логин",
modifier = Modifier.align(Alignment.CenterHorizontally)
)
BasicTextField(
value = viewModel.userUiState.details.login,
onValueChange = {
viewModel.updateUiState(viewModel.userUiState.details.copy(login = it))
},
if (user.value != null) {
Column(
modifier = Modifier
.fillMaxWidth()
.size(36.dp)
.background(MaterialTheme.colorScheme.secondary, RoundedCornerShape(18.dp))
.padding(start = 13.dp, top = 8.dp)
)
Text(
text = "Пароль",
modifier = Modifier.align(Alignment.CenterHorizontally)
)
BasicTextField(
value = viewModel.userUiState.details.password,
onValueChange = {
viewModel.updateUiState(viewModel.userUiState.details.copy(password = it))
},
modifier = Modifier
.fillMaxWidth()
.size(36.dp)
.background(MaterialTheme.colorScheme.secondary, RoundedCornerShape(18.dp))
.padding(start = 13.dp, top = 8.dp),
visualTransformation = PasswordVisualTransformation()
)
if (isRegistration) {
.padding(16.dp),
) {
Text(
text = "Подтверждение пароля",
text = "Текущий пользователь: " + (user.value?.login ?: ""),
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Button(
enabled = user.value != null,
onClick = {
coroutine.launch {
dataStoreManager.setLogin("")
}
},
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) { Text("Выход") }
}
} else {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = errorMessage,
color = Color.Red
)
Text(
text = "Логин",
modifier = Modifier.align(Alignment.CenterHorizontally)
)
BasicTextField(
value = viewModel.userUiState.details.passwordConfirm,
value = viewModel.userUiState.details.login,
onValueChange = {
viewModel.updateUiState(
viewModel.userUiState.details.copy(
passwordConfirm = it
)
viewModel.updateUiState(viewModel.userUiState.details.copy(login = it))
},
modifier = Modifier
.fillMaxWidth()
.size(36.dp)
.background(
MaterialTheme.colorScheme.secondary,
RoundedCornerShape(18.dp)
)
.padding(start = 13.dp, top = 8.dp)
)
Text(
text = "Пароль",
modifier = Modifier.align(Alignment.CenterHorizontally)
)
BasicTextField(
value = viewModel.userUiState.details.password,
onValueChange = {
viewModel.updateUiState(viewModel.userUiState.details.copy(password = it))
},
modifier = Modifier
.fillMaxWidth()
@ -137,89 +123,110 @@ fun UserProfile(
.padding(start = 13.dp, top = 8.dp),
visualTransformation = PasswordVisualTransformation()
)
}
if (isRegistration) {
Button(
onClick = {
coroutineScope.launch {
val flag = viewModel.signUp()
isRegistration = !flag
}
},
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
Text("Регистрация")
if (isRegistration) {
Text(
text = "Подтверждение пароля",
modifier = Modifier.align(Alignment.CenterHorizontally)
)
BasicTextField(
value = viewModel.userUiState.details.passwordConfirm,
onValueChange = {
viewModel.updateUiState(
viewModel.userUiState.details.copy(
passwordConfirm = it
)
)
},
modifier = Modifier
.fillMaxWidth()
.size(36.dp)
.background(
MaterialTheme.colorScheme.secondary,
RoundedCornerShape(18.dp)
)
.padding(start = 13.dp, top = 8.dp),
visualTransformation = PasswordVisualTransformation()
)
}
Text(
text = "Уже есть аккаунт? Войти",
modifier = Modifier
.clickable {
isRegistration = false
}
.align(Alignment.CenterHorizontally),
color = MaterialTheme.colorScheme.onBackground
)
} else {
Button(
onClick = {
coroutineScope.launch {
if (viewModel.signIn(dataStoreManager)) {
navController.navigate(Screen.CinemaList.route)
if (isRegistration) {
Button(
onClick = { coroutine.launch { isRegistration = !viewModel.signUp() } },
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
Text("Регистрация")
}
Text(
text = "Уже есть аккаунт? Войти",
modifier = Modifier
.clickable {
isRegistration = false
}
}
},
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
Text("Вход")
.align(Alignment.CenterHorizontally),
color = MaterialTheme.colorScheme.onBackground
)
} else {
Button(
onClick = {
coroutine.launch {
if (viewModel.signIn(dataStoreManager)) {
navController.navigate(Screen.CinemaList.route)
}
}
},
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
Text("Вход")
}
Text(
text = "Нет аккаунта? Зарегистрироваться",
modifier = Modifier
.clickable {
isRegistration = true
}
.align(Alignment.CenterHorizontally),
color = MaterialTheme.colorScheme.onBackground
)
}
Text(
text = "Нет аккаунта? Зарегистрироваться",
modifier = Modifier
.clickable {
isRegistration = true
}
.align(Alignment.CenterHorizontally),
color = MaterialTheme.colorScheme.onBackground
)
}
val switchColors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colorScheme.primary, // Change the color when the switch is checked
checkedTrackColor = MaterialTheme.colorScheme.secondary, // Change the color of the track when the switch is checked
uncheckedThumbColor = MaterialTheme.colorScheme.primary, // Change the color when the switch is unchecked
uncheckedTrackColor = MaterialTheme.colorScheme.onPrimary // Change the color of the track when the switch is unchecked
}
val switchColors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colorScheme.primary, // Change the color when the switch is checked
checkedTrackColor = MaterialTheme.colorScheme.secondary, // Change the color of the track when the switch is checked
uncheckedThumbColor = MaterialTheme.colorScheme.primary, // Change the color when the switch is unchecked
uncheckedTrackColor = MaterialTheme.colorScheme.onPrimary // Change the color of the track when the switch is unchecked
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.End
) {
Text(
"Темная тема", modifier = Modifier
.align(Alignment.CenterVertically)
.padding(5.dp)
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.End
) {
Text(
"Темная тема", modifier = Modifier
.align(Alignment.CenterVertically)
.padding(5.dp)
)
Switch(
checked = isDarkTheme.value,
onCheckedChange = {
isDarkTheme.value = !isDarkTheme.value
coroutine.launch {
if (isDarkTheme.value) {
dataStoreManager.setDarkTheme("Dark")
} else {
dataStoreManager.setDarkTheme("Light")
}
Switch(
checked = isDarkTheme.value,
onCheckedChange = {
isDarkTheme.value = !isDarkTheme.value
coroutine.launch {
if (isDarkTheme.value) {
dataStoreManager.setDarkTheme("Dark")
} else {
dataStoreManager.setDarkTheme("Light")
}
},
colors = switchColors
)
}
}
},
colors = switchColors
)
}
}
}

View File

@ -12,6 +12,7 @@
<string name="Order_title">Мои заказы</string>
<string name="Profile_title">Профиль</string>
<string name="Sessions_title">Сеансы</string>
<string name="Report_title">Отчет</string>
<string name="Session_dateTime">Время</string>
<string name="Save_button">Сохранить</string>
<string name="Cinema_empty_description">Записи о фильмах отсутствуют</string>

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
"name": "fake-db",
"version": "1.0.0",
"scripts": {
"start": "json-server --watch data.json --host 0.0.0.0 -p 8079"
"start": "json-server --watch data.json --middlewares ./reportRouter.js --host 0.0.0.0 -p 8079"
},
"dependencies": {
},

62
server/reportRouter.js Normal file
View File

@ -0,0 +1,62 @@
module.exports = (req, res, next) => {
const isReportRequest = req.url.startsWith('/report') && req.method === 'GET';
if (!isReportRequest) {
next();
return;
}
try {
const { startDate, endDate } = req.query;
const { sessions, orders } = require('./data.json');
const start = new Date(startDate);
const end = new Date(endDate);
// Фильтруем сеансы по периоду
const filteredSessions = sessions.filter(session => {
const sessionDate = new Date(session.dateTime.replace(/(\d{2}).(\d{2}).(\d{4}) (\d{2}):(\d{2})/, '$3-$2-$1T$4:$5'));
// обнуление времени с учетом зоны времени
sessionDate.setHours(4, 0, 0, 0);
return sessionDate >= start && sessionDate <= end;
});
// Обрабатываем отфильтрованные сеансы для аналитики
const reportData = filteredSessions.map(session => {
// берем заказ, где сеанс только текущий
const relevantOrders = orders
.map(orderWithOneSession => ({
...orderWithOneSession,
sessions: orderWithOneSession.sessions.filter(orderSession => orderSession.id === session.id &&
orderSession.cinemaId === session.cinemaId && orderSession.dateTime === session.dateTime)
})).filter(order => order.sessions.length > 0);
const { totalTicketsSold, revenue } = relevantOrders.reduce((accumulator, order) => {
const session = order.sessions[0];
const tickets = session.count;
const sessionRevenue = tickets * session.frozenPrice;
return {
totalTicketsSold: accumulator.totalTicketsSold + tickets,
revenue: accumulator.revenue + sessionRevenue
};
}, { totalTicketsSold: 0, revenue: 0 });
return {
cinema_name: relevantOrders[0].sessions[0].cinema.name,
current_ticket_date_time: session.dateTime,
current_ticket_price: session.price,
max_ticket_quantity: session.maxCount,
purchased_tickets: totalTicketsSold,
revenue: revenue
};
});
const sortedReportData = reportData.sort((a, b) => b.revenue - a.revenue);
res.json(sortedReportData);
} catch (error) {
console.error('Error processing report: ', error);
res.status(500).json({ message: 'Internal Server Error' });
}
};