Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
Empty file.
10 changes: 3 additions & 7 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.plugin.compose'
}

android {
Expand Down Expand Up @@ -46,10 +47,6 @@ android {
buildConfig true
}

composeOptions {
kotlinCompilerExtensionVersion '1.5.14'
}

packaging {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
Expand All @@ -72,12 +69,13 @@ dependencies {
implementation 'androidx.compose.foundation:foundation'
implementation 'androidx.compose.material3:material3'
implementation 'androidx.compose.material:material-icons-extended'
implementation 'androidx.compose.runtime:runtime-saveable'
implementation 'androidx.compose.runtime:runtime-livedata'

debugImplementation 'androidx.compose.ui:ui-tooling'
debugImplementation 'androidx.compose.ui:ui-test-manifest'

implementation 'androidx.navigation:navigation-compose:2.8.5'

implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7'

implementation 'com.squareup.retrofit2:retrofit:2.11.0'
Expand All @@ -100,6 +98,4 @@ dependencies {
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'

implementation 'androidx.compose.runtime:runtime-livedata'
}
11 changes: 10 additions & 1 deletion app/src/main/java/com/example/goodroad/data/network/ApiClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ import retrofit2.*
import retrofit2.converter.gson.*
import java.time.Instant
import java.util.concurrent.*
import com.example.goodroad.data.network.GoodRoadApi
import com.example.goodroad.modules.auth.data.AuthApi
import com.example.goodroad.modules.review.data.ReviewApi
import com.example.goodroad.modules.user.data.UserApi
import com.example.goodroad.modules.volunteer.data.VolunteerApi
import com.example.goodroad.modules.moderator.data.VolunteerModerationApi

object ApiClient {

Expand Down Expand Up @@ -96,5 +97,13 @@ object ApiClient {
val routeApi: GoodRoadApi by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
retrofit().create(GoodRoadApi::class.java)
}

val volunteerApi: VolunteerApi by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
retrofit().create(VolunteerApi::class.java)
}

val volunteerModerationApi: VolunteerModerationApi by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
retrofit().create(VolunteerModerationApi::class.java)
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ import com.example.goodroad.modules.moderationReview.presentation.ReviewModerati
import com.example.goodroad.modules.moderator.screens.AdminProfileScreen
import com.example.goodroad.modules.moderator.screens.ModeratorProfileScreen
import com.example.goodroad.modules.moderator.screens.ModeratorsManagementScreen
import com.example.goodroad.modules.moderator.screens.VolunteerManagementScreen
import com.example.goodroad.modules.moderationReview.screens.ReviewModerationScreen
import com.example.goodroad.modules.moderator.data.VolunteerModerationRepository
import com.example.goodroad.modules.moderator.presentation.VolunteerModerationViewModel

@Composable
fun AuthApp(
Expand All @@ -38,6 +41,7 @@ fun AuthApp(
navController = navController,
startDestination = LOGIN_ROUTE
) {

composable(LOGIN_ROUTE) {
LoginScreen(
onLoginSuccess = { resp ->
Expand Down Expand Up @@ -95,6 +99,7 @@ fun AuthApp(
}

composable("admin_home") {

val userViewModel: UserViewModel = viewModel(factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return UserViewModel(UserRepository(ApiClient.userApi)) as T
Expand All @@ -105,6 +110,9 @@ fun AuthApp(
userViewModel = userViewModel,
onModerators = { navController.navigate("moderators") },
onReviews = { navController.navigate("review_moderation") },
onVolunteers = {
navController.navigate("admin_volunteers")
},
onLogout = {
navController.navigate(LOGIN_ROUTE) {
popUpTo("admin_home") { inclusive = true }
Expand All @@ -113,7 +121,26 @@ fun AuthApp(
)
}

composable("admin_volunteers") {

val volunteerModerationViewModel: VolunteerModerationViewModel = viewModel(
factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return VolunteerModerationViewModel(
VolunteerModerationRepository()
) as T
}
}
)

VolunteerManagementScreen(
viewModel = volunteerModerationViewModel,
onBack = { navController.popBackStack() }
)
}

composable("moderator_home") {

val userViewModel: UserViewModel = viewModel(factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return UserViewModel(UserRepository(ApiClient.userApi)) as T
Expand All @@ -123,6 +150,7 @@ fun AuthApp(
ModeratorProfileScreen(
userViewModel = userViewModel,
onReviews = { navController.navigate("review_moderation") },
onVolunteers = { navController.navigate("volunteers") },
onLogout = {
navController.navigate(LOGIN_ROUTE) {
popUpTo("moderator_home") { inclusive = true }
Expand All @@ -132,6 +160,7 @@ fun AuthApp(
}

composable("moderators") {

val moderatorViewModel: ModeratorViewModel = viewModel(factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ModeratorViewModel(ModeratorRepository()) as T
Expand All @@ -144,8 +173,28 @@ fun AuthApp(
)
}

composable("volunteers") {

val volunteerModerationViewModel: VolunteerModerationViewModel = viewModel(
factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return VolunteerModerationViewModel(
VolunteerModerationRepository()
) as T
}
}
)

VolunteerManagementScreen(
viewModel = volunteerModerationViewModel,
onBack = { navController.popBackStack() }
)
}

composable("review_moderation") {

val moderationRepository = ModerationReviewRepository(ApiClient.moderationReviewApi)

val moderationViewModel: ReviewModerationViewModel = viewModel(factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ReviewModerationViewModel(moderationRepository) as T
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.example.goodroad.modules.moderator.data

import retrofit2.Response
import retrofit2.http.*

interface VolunteerModerationApi {

@GET("volunteer/moderation/applications/pending")
suspend fun getPendingApplications(): Response<List<VolunteerApplicationResp>>

@POST("volunteer/moderation/applications/{id}/approve")
suspend fun approve(
@Path("id") id: String
): Response<VolunteerApplicationResp>

@POST("volunteer/moderation/applications/{id}/reject")
suspend fun reject(
@Path("id") id: String,
@Body req: RejectReq
): Response<VolunteerApplicationResp>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.example.goodroad.modules.moderator.data

data class VolunteerApplicationResp(
val id: String,
val applicantId: String,
val applicantName: String,
val dobroUrl: String?,
val phone: String,
val socialNickname: String?,
val certificatePhotoUrls: List<String> = emptyList(),
val status: String,
val moderatorComment: String?,
val createdAt: String?,
val moderatedAt: String?
)

data class RejectReq(
val reason: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.example.goodroad.modules.moderator.data

import com.example.goodroad.data.network.ApiClient
import retrofit2.HttpException

class VolunteerModerationRepository {

private val api = ApiClient.volunteerModerationApi

suspend fun getPendingApplications(): List<VolunteerApplicationResp> {
val response = api.getPendingApplications()

if (response.isSuccessful) {
return response.body().orEmpty()
}

throw HttpException(response)
}

suspend fun approve(id: String) {
val response = api.approve(id)
if (!response.isSuccessful) throw HttpException(response)
}

suspend fun reject(id: String, reason: String) {
val response = api.reject(id, RejectReq(reason))
if (!response.isSuccessful) throw HttpException(response)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.example.goodroad.modules.moderator.presentation

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.goodroad.modules.moderator.data.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch

class VolunteerModerationViewModel(
private val repo: VolunteerModerationRepository
) : ViewModel() {

private val _applications = MutableStateFlow<List<VolunteerApplicationResp>>(emptyList())
val applications: StateFlow<List<VolunteerApplicationResp>> = _applications

private val _loading = MutableStateFlow(false)
val loading: StateFlow<Boolean> = _loading

private val _error = MutableStateFlow<String?>(null)
val error: StateFlow<String?> = _error

fun load() {
viewModelScope.launch {
_loading.value = true
try {
_applications.value = repo.getPendingApplications()
} catch (e: Exception) {
_error.value = e.message ?: "Ошибка загрузки"
} finally {
_loading.value = false
}
}
}

fun approve(id: String) {
viewModelScope.launch {
try {
repo.approve(id)
load()
} catch (e: Exception) {
_error.value = e.message ?: "Ошибка approve"
}
}
}

fun reject(id: String, reason: String) {
viewModelScope.launch {
try {
repo.reject(id, reason)
load()
} catch (e: Exception) {
_error.value = e.message ?: "Ошибка reject"
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ fun AdminProfileScreen(
userViewModel: UserViewModel,
onModerators: () -> Unit,
onReviews: () -> Unit,
onVolunteers: () -> Unit,
onLogout: () -> Unit
) {

Expand Down Expand Up @@ -109,6 +110,16 @@ fun AdminProfileScreen(

Spacer(Modifier.height(12.dp))

PrimaryButton(
text = "Волонтёры",
backgroundColor = UrbanBrown,
contentColor = WhiteSoft
) {
onVolunteers()
}

Spacer(Modifier.height(12.dp))

PrimaryButton(
text = "Отзывы",
backgroundColor = UrbanBrown,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.example.goodroad.ui.UserDecor
fun ModeratorProfileScreen(
userViewModel: UserViewModel,
onReviews: () -> Unit,
onVolunteers: () -> Unit,
onLogout: () -> Unit
) {

Expand Down Expand Up @@ -94,6 +95,16 @@ fun ModeratorProfileScreen(

Spacer(Modifier.height(30.dp))

PrimaryButton(
text = "Волонтёры",
backgroundColor = UrbanBrown,
contentColor = WhiteSoft
) {
onVolunteers()
}

Spacer(Modifier.height(12.dp))

PrimaryButton(
text = "Отзывы",
backgroundColor = UrbanBrown,
Expand Down
Loading