Report is working.

This commit is contained in:
ElEgEv 2023-12-24 13:16:15 +04:00
parent 2b69f0fc24
commit 0db26bb7a2
25 changed files with 583 additions and 15 deletions

Binary file not shown.

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="ModuleClassLoaderOverlays">
<paths>
<option value="C:\Users\egore\AppData\Local\Temp\overlay18388947515922838363" />
<option value="C:\Users\egore\AppData\Local\Temp\overlay18323405549060409157" />
<option value="C:\Users\egore\AppData\Local\Temp\overlay16485769531165389458" />
<option value="C:\Users\egore\AppData\Local\Temp\overlay7802277587693817774" />
<option value="C:\Users\egore\AppData\Local\Temp\overlay16197097467506260191" />
</paths>
</component>
</module>

View File

@ -68,6 +68,7 @@ import ru.ulstu.`is`.pmu.tanks.composeui.Account
import ru.ulstu.`is`.pmu.tanks.composeui.Constructor
import ru.ulstu.`is`.pmu.tanks.composeui.Hangar
import ru.ulstu.`is`.pmu.tanks.composeui.NationList
import ru.ulstu.`is`.pmu.tanks.composeui.Report
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -150,6 +151,7 @@ fun Navhost(
Screen.Constructor.route,
arguments = listOf(navArgument("id") { type = NavType.LongType })
) { Constructor(navController) }
composable(Screen.Report.route) { Report(navController) }
composable(Screen.Hangar.route) { Hangar(navController) }
composable(Screen.Account.route) { Account(navController) }
}
@ -261,6 +263,19 @@ fun MainNavbar(navController: NavController) {
) {
Text(text = "Добавить")
}
Button(
modifier = Modifier
.width(200.dp)
.height(50.dp),
colors = ButtonDefaults.buttonColors(
containerColor = CustomOrange,
contentColor = CustomDark),
onClick = {
navController.navigate(Screen.Report.route)
}
) {
Text("Отчёты")
}
}
Navhost(navController)
}

View File

@ -7,6 +7,7 @@ import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Create
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.List
import androidx.compose.ui.graphics.vector.ImageVector
import ru.ulstu.`is`.pmu.R
@ -38,6 +39,9 @@ enum class Screen(
StudentView(
"student-view/{id}", R.string.student_view_title, showInBottomBar = false
),
Report(
"report", R.string.report, showInBottomBar = false
),
NATIONS("nations", R.string.nation, Icons.Filled.Create),
EDIT_NATIONS("edit-nation/{id}", R.string.nation);

View File

@ -7,5 +7,5 @@ object ApiRoutes {
const val NATION = "nations"
const val TANK = "tanks"
const val USER_TANK = "users_tanks"
const val NOT_USER_TANK = "notUserTanks"
const val REPORT = "report"
}

View File

@ -15,10 +15,12 @@ import retrofit2.http.Path
import retrofit2.http.Query
import ru.ulstu.`is`.pmu.tank.api.model.LevelRemote
import ru.ulstu.`is`.pmu.tank.api.model.NationRemote
import ru.ulstu.`is`.pmu.tank.api.model.ReportRemote
import ru.ulstu.`is`.pmu.tank.api.model.TankRemote
import ru.ulstu.`is`.pmu.tank.api.model.TankWithNationAndLevelRemote
import ru.ulstu.`is`.pmu.tank.api.model.UserRemote
import ru.ulstu.`is`.pmu.tank.api.model.UserTankCrossRefRemote
import java.util.Date
interface ServerService {
// :[USER]
@ -145,6 +147,12 @@ interface ServerService {
@Path("id") id: Long
): TankRemote
@GET("${ApiRoutes.REPORT}")
suspend fun getReportInfo(
@Query("startDate") startDate: Date,
@Query("endDate") endDate: Date
): ReportRemote
// ![TANK]
// :[USER_TANK_CROSS_REF]
@ -166,6 +174,7 @@ interface ServerService {
// ![USER_TANK_CROSS_REF]
companion object {
private const val BASE_URL = ApiRoutes.BASE

View File

@ -0,0 +1,42 @@
package ru.ulstu.`is`.pmu.tank.api.model
import com.application.ui.toBitmap
import kotlinx.serialization.Serializable
import ru.ulstu.`is`.pmu.tank.model.Report
import ru.ulstu.`is`.pmu.tank.model.Tank
@Serializable
data class ReportRemote(
//tank part
val id: Int = 0,
val tankName: String = "",
val price: Int = 0,
val miniature: String = "",
val tankLevel: Int = 0,
val tankNation: String = "",
val countTankPurchase: Int = 0,
//level part
val level: Int = 0,
val countLevelPurchase: Int = 0,
//nation part
val nationName: String = "",
val countNationPurchase: Int = 0,
){ }
fun ReportRemote.toReport(): Report = Report(
id = id,
tankName = tankName,
price = price,
miniature = miniature.toBitmap(),
tankLevel = tankLevel,
tankNation = tankNation,
countTankPurchase = countTankPurchase,
level = level,
countLevelPurchase = countLevelPurchase,
nationName = nationName,
countNationPurchase = countNationPurchase
)

View File

@ -7,15 +7,18 @@ import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList
import ru.ulstu.`is`.pmu.tank.api.ServerService
import ru.ulstu.`is`.pmu.tank.api.model.ReportRemote
import ru.ulstu.`is`.pmu.tank.api.model.UserTankCrossRefRemote
import ru.ulstu.`is`.pmu.tank.api.model.toLevel
import ru.ulstu.`is`.pmu.tank.api.model.toNation
import ru.ulstu.`is`.pmu.tank.api.model.toRemote
import ru.ulstu.`is`.pmu.tank.api.model.toReport
import ru.ulstu.`is`.pmu.tank.api.model.toTank
import ru.ulstu.`is`.pmu.tank.api.model.toTankWithNationAndLevel
import ru.ulstu.`is`.pmu.tank.api.model.toUserTankCrossRef
import ru.ulstu.`is`.pmu.tank.model.Level
import ru.ulstu.`is`.pmu.tank.model.Nation
import ru.ulstu.`is`.pmu.tank.model.Report
import ru.ulstu.`is`.pmu.tank.model.Tank
import ru.ulstu.`is`.pmu.tank.model.TankWithNationAndLevel
import ru.ulstu.`is`.pmu.tank.model.UserTankCrossRef
@ -67,6 +70,10 @@ class RestTankRepository (
return service.getTank(uid).toTank()
}
suspend fun getReport(startDate: Date, endDate: Date): Report {
return service.getReportInfo(startDate, endDate).toReport();
}
override suspend fun getUserTanks(userId: Long): List<TankWithNationAndLevel> {
val totalList: List<Tank> = getAll()
val totalLevelList: List<Level> = service.getLevels().map { it.toLevel() }

View File

@ -0,0 +1,3 @@
package ru.ulstu.`is`.pmu.tank.composeui
enum class ApiStatus { LOADING, ERROR, DONE }

View File

@ -0,0 +1,47 @@
package ru.ulstu.`is`.pmu.tank.composeui
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import retrofit2.HttpException
import java.io.IOException
open class MyViewModel : ViewModel() {
var apiStatus by mutableStateOf(ApiStatus.DONE)
private set
var apiError by mutableStateOf("")
private set
fun runInScope(
actionSuccess: suspend () -> Unit,
actionError: suspend () -> Unit
) {
viewModelScope.launch {
apiStatus = ApiStatus.LOADING
runCatching {
actionSuccess()
apiStatus = ApiStatus.DONE
apiError = ""
}.onFailure { e: Throwable ->
when (e) {
is IOException,
is HttpException -> {
actionError()
apiStatus = ApiStatus.ERROR
apiError = e.localizedMessage ?: e.toString()
}
else -> throw e
}
}
}
}
fun runInScope(actionSuccess: suspend () -> Unit) {
runInScope(actionSuccess, actionError = {})
}
}

View File

@ -0,0 +1,63 @@
package ru.ulstu.`is`.pmu.tank.composeui.edit
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import ru.ulstu.`is`.pmu.tank.api.model.ReportRemote
import ru.ulstu.`is`.pmu.tank.api.repository.RestTankRepository
import ru.ulstu.`is`.pmu.tank.composeui.MyViewModel
import ru.ulstu.`is`.pmu.tank.model.Report
import java.util.Date
class ReportViewModel (private val tankRepository: RestTankRepository): MyViewModel()
{
var reportPageUiState by mutableStateOf(ReportPageUiState())
private set
var reportResultPageUiState by mutableStateOf(ReportResultPageUiState())
private set
fun onUpdate(reportDetails: ReportDetails)
{
reportPageUiState = ReportPageUiState(reportDetails = reportDetails,isEntryValid = validateInput(reportDetails))
}
private fun validateInput(uiState: ReportDetails = reportPageUiState.reportDetails): Boolean {
Log.d("Checking",uiState.endDate.toString())
return with(uiState) {
startDate!=Date(0)
&& endDate!=Date(0)
&& startDate < endDate
}
}
suspend fun getReport(){
runInScope(
actionSuccess = {
val res = tankRepository.getReport(reportPageUiState.reportDetails.startDate, reportPageUiState.reportDetails.endDate)
Log.d("MAIN CHECKING", res.toString())
reportResultPageUiState = ReportResultPageUiState(res)
},
actionError = {
reportResultPageUiState = ReportResultPageUiState()
}
)
}
}
data class ReportDetails(
val startDate: Date = Date(0),
val endDate: Date = Date(0)
)
data class ReportPageUiState(
val reportDetails: ReportDetails = ReportDetails(),
val isEntryValid: Boolean = false
)
data class ReportResultPageUiState(
var resReport: Report? = null
)

View File

@ -52,7 +52,7 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun remoteKeysDao(): RemoteKeysDao
companion object {
private const val DB_NAME: String = "23-db"
private const val DB_NAME: String = "24-db"
@Volatile
private var INSTANCE: AppDatabase? = null

View File

@ -0,0 +1,26 @@
package ru.ulstu.`is`.pmu.tank.model
import android.graphics.Bitmap
import androidx.room.ColumnInfo
import androidx.room.Ignore
import androidx.room.PrimaryKey
import com.application.ui.getEmptyBitmap
data class Report (
//tank part
val id: Int,
val tankName: String,
val price: Int,
val miniature: Bitmap,
val tankLevel: Int,
val tankNation: String,
val countTankPurchase: Int,
//level part
val level: Int,
val countLevelPurchase: Int,
//nation part
val nationName: String,
val countNationPurchase: Int,
) { }

View File

@ -0,0 +1,231 @@
package ru.ulstu.`is`.pmu.tanks.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.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
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.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.application.ui.toBitmap
import kotlinx.coroutines.launch
import ru.ulstu.`is`.pmu.tank.api.model.ReportRemote
import ru.ulstu.`is`.pmu.tank.composeui.ApiStatus
import ru.ulstu.`is`.pmu.tank.composeui.edit.ReportViewModel
import ru.ulstu.`is`.pmu.tank.model.Report
import ru.ulstu.`is`.pmu.tanks.composeui.image.CuteImage
import ru.ulstu.`is`.pmu.ui.AppViewModelProvider
import ru.ulstu.`is`.pmu.ui.theme.CustomRed
import ru.ulstu.`is`.pmu.ui.theme.CustomYellow
import java.util.Date
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Report (navController: NavController?,viewModel: ReportViewModel = viewModel(factory = AppViewModelProvider.Factory))
{
when (viewModel.apiStatus) {
ApiStatus.DONE -> {
val dateStateStart = rememberDatePickerState(initialDisplayMode = DisplayMode.Input)
val dateStateEnd = rememberDatePickerState(initialDisplayMode = DisplayMode.Input)
val coroutineScope = rememberCoroutineScope()
val reportResultPageState = viewModel.reportResultPageUiState
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
)
{
Text(
text = "Начало периода",
style = MaterialTheme.typography.headlineLarge
)
DatePicker(
state = dateStateStart,
)
val selectedDateStart = dateStateStart.selectedDateMillis
if (selectedDateStart != null) {
val resultDate= Date(selectedDateStart)
viewModel.onUpdate(viewModel.reportPageUiState.reportDetails.copy(startDate = resultDate))
}
else
{
viewModel.onUpdate(viewModel.reportPageUiState.reportDetails.copy(startDate = Date(0)))
}
Text(
text = "Конец периода",
style = MaterialTheme.typography.headlineLarge
)
DatePicker(
state = dateStateEnd,
)
val selectedDateEnd = dateStateEnd.selectedDateMillis
if (selectedDateEnd != null) {
val resultDate = Date(selectedDateEnd)
viewModel.onUpdate(viewModel.reportPageUiState.reportDetails.copy(endDate = resultDate))
}
else
{
viewModel.onUpdate(viewModel.reportPageUiState.reportDetails.copy(endDate = Date(0)))
}
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = {coroutineScope.launch { viewModel.getReport() } },
enabled = viewModel.reportPageUiState.isEntryValid,
modifier = Modifier
.fillMaxWidth()
.height(56.dp)
.clip(RoundedCornerShape(8.dp)),
colors = ButtonDefaults.buttonColors(
CustomRed, Color.White, CustomYellow,
CustomYellow
),
) {
Text("Сформировать отчет")
}
Spacer(modifier = Modifier.height(32.dp))
Text(
text = "Результат",
style = MaterialTheme.typography.headlineLarge
)
if(reportResultPageState.resReport != null){
TableScreen(reportData = reportResultPageState.resReport!!)
}
}
}
ApiStatus.LOADING -> LoadingPlaceholder()
else -> ErrorPlaceholder(
message = viewModel.apiError,
onBack = {
navController?.popBackStack()
}
)
}
}
@Composable
fun LoadingPlaceholder() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(10.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
fontSize = TextUnit(value = 25F, type = TextUnitType.Sp),
text = "Загрузка"
)
}
}
@Composable
fun ErrorPlaceholder(message: String, onBack: () -> Unit) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(10.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
fontSize = TextUnit(value = 20F, type = TextUnitType.Sp),
text = message,
color = Color(0xFFFF1744)
)
Spacer(modifier = Modifier.padding(bottom = 10.dp))
Button(
modifier = Modifier.fillMaxWidth(),
onClick = { onBack() }
) {
Text("Назад")
}
}
}
@Composable
fun RowScope.TableCell(
text: String,
weight: Float
) {
Text(
text = text,
Modifier
.border(1.dp, Color.Black)
.weight(weight)
.padding(8.dp)
)
}
@Composable
fun TableScreen(reportData: Report) {
val column1Weight = .3f // 30%
Column(
Modifier
.padding(16.dp)) {
Row(Modifier.background(Color.Gray)) {
TableCell(text = "Название танка:", weight = column1Weight)
TableCell(text = "Стоимость танка:", weight = column1Weight)
//TableCell(text = "Изображение:", weight = column1Weight)
TableCell(text = "Уровень:", weight = column1Weight)
TableCell(text = "Нация:", weight = column1Weight)
TableCell(text = "Кол-во покупок:", weight = column1Weight)
TableCell(text = "Самый популярный уровень:", weight = column1Weight)
TableCell(text = "Кол-во покупок:", weight = column1Weight)
TableCell(text = "Самая популярная нация:", weight = column1Weight)
TableCell(text = "Кол-во покупок:", weight = column1Weight)
}
// Here are all the lines of your table.
Row(Modifier.fillMaxWidth()) {
TableCell(text = reportData.tankName, weight = column1Weight)
TableCell(text = reportData.price.toString(), weight = column1Weight)
CuteImage(
imageBitmap = reportData.miniature.asImageBitmap(),
modifier = Modifier.fillMaxWidth()
)
TableCell(text = reportData.tankLevel.toString(), weight = column1Weight)
TableCell(text = reportData.tankNation, weight = column1Weight)
TableCell(text = reportData.countTankPurchase.toString(), weight = column1Weight)
TableCell(text = reportData.level.toString(), weight = column1Weight)
TableCell(text = reportData.countLevelPurchase.toString(), weight = column1Weight)
TableCell(text = reportData.nationName, weight = column1Weight)
TableCell(text = reportData.countNationPurchase.toString(), weight = column1Weight)
}
}
}

View File

@ -9,6 +9,7 @@ import ru.ulstu.`is`.pmu.TankApplication
import ru.ulstu.`is`.pmu.tank.composeui.edit.LevelDropDownViewModel
import ru.ulstu.`is`.pmu.tank.composeui.edit.NationDropDownViewModel
import ru.ulstu.`is`.pmu.tank.composeui.edit.NationsListUiState
import ru.ulstu.`is`.pmu.tank.composeui.edit.ReportViewModel
import ru.ulstu.`is`.pmu.tank.composeui.edit.TankEditViewModel
import ru.ulstu.`is`.pmu.tank.composeui.edit.UserEditViewModel
import ru.ulstu.`is`.pmu.tank.composeui.edit.UsersTanksEditViewModel
@ -41,6 +42,11 @@ object AppViewModelProvider {
tankApplication().container.usersTanksRepository
)
}
initializer {
ReportViewModel(
tankApplication().container.tankRestRepository
)
}
initializer {
LevelDropDownViewModel(tankApplication().container.levelRestRepository)
}

View File

@ -22,6 +22,7 @@
<string name="student_email">e-mail</string>
<string name="tanks_main_title">Главная</string>
<string name="student_view_title">Профиль студента</string>
<string name="report">Отчёты</string>
<string name="generator">Генератор</string>
<string name="main_label">Список техники по:</string>
<string name="t_34_85">T-34-85</string>

View File

@ -2,7 +2,7 @@
"name": "20-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": {
},

View File

@ -0,0 +1,126 @@
module.exports = (req, res, next) => {
if (req.url.startsWith('/report') && req.method === 'GET') {
const { startDate, endDate } = req.query;
try {
delete require.cache[require.resolve('./data.json')];
const data = require('./data.json');
const filteredPurchase = data.users_tanks.filter(purchase => {
const purchaseDate = new Date(purchase.date);
return purchaseDate >= new Date(startDate) && purchaseDate <= new Date(endDate);
});
//ПОИСК САМОГО ПОПУЛЯРНОГО ТАНКА ЗА ПЕРИОД
//список танков с кол-во их покупок
let tankList = [];
filteredPurchase.forEach(purchase => {
const tankId = purchase.tankId;
const tankInList = tankList.some(item => item.tankId === tankId);
if (tankInList) {
tankList[tankList.findIndex(index => index.tankId === tankId)].count++;
} else {
tankList.push(
{
tankId: tankId,
count: 1
}
)
}
});
tankList.sort((a, b) => b.count - a.count);
//нашли самый популярный танк
const supportTank = data.tanks.find(tank => tank.id === tankList[0].tankId);
const supportLevel = data.levels.find(level => level.id === supportTank.levelId).level;
const supportNation = data.nations.find(nation => nation.id === supportTank.levelId).nationName;
const popularTank = {
id: supportTank.id,
name: supportTank.name,
price: supportTank.price,
miniature: supportTank.miniature,
level: supportLevel,
nation: supportNation
};
//ПОИСК САМЫХ ПОПУЛЯРНЫХ УРОВНЯ И НАЦИИ ЗА ПЕРИОД
//список уроней и кол-во их покупок
let levelList = [];
//список уроней и кол-во их покупок
let nationList = [];
tankList.forEach(element => {
const tank = data.tanks.find(tank => tank.id === element.tankId);
//смотрим, сколько раз танк был куплен, чтобы сразу прибавить к соответствующему уровню текущее кол-во
const countCurrentTank = element.count;
const levelId = tank.levelId;
const nationId = tank.nationId;
const levelInList = levelList.some(item => item.levelId === levelId);
const nationInList = nationList.some(item => item.nationId === nationId);
if (levelInList) {
levelList[levelList.findIndex(index => index.levelId === levelId)].count += countCurrentTank;
} else {
levelList.push(
{
levelId: levelId,
count: countCurrentTank
}
)
}
if (nationInList) {
nationList[nationList.findIndex(index => index.nationId === nationId)].count += countCurrentTank;
} else {
nationList.push(
{
nationId: nationId,
count: countCurrentTank
}
)
}
});
levelList.sort((a, b) => b.count - a.count);
nationList.sort((a, b) => b.count - a.count);
//нашли самые популярные уровень и нацию
const popularLevel = data.levels.find(level => level.id === levelList[0].levelId)
const popularNation = data.nations.find(nation => nation.id === nationList[0].nationId)
finalDataReport = {
id: popularTank.id,
tankName: popularTank.name,
price: popularTank.price,
miniature: popularTank.miniature,
tankLevel: popularTank.level,
tankNation: popularTank.nation,
countTankPurchase: tankList[0].count,
level: popularLevel.level,
countLevelPurchase: levelList[0].count,
nationName: popularNation.nationName,
countNationPurchase: nationList[0].count
};
console.log(finalDataReport)
res.json(finalDataReport);
} catch (error) {
console.error('Error loading data:', error);
res.status(500).json({ message: 'Internal Server Error' });
}
} else {
next();
}
};